diff --git a/backend/xray/config.go b/backend/xray/config.go index 9ffa05e..b54753c 100644 --- a/backend/xray/config.go +++ b/backend/xray/config.go @@ -309,102 +309,131 @@ func (i *Inbound) removeUser(email string) { type Stats struct{} -func (c *Config) ToBytes() ([]byte, error) { - // Acquire read locks for all inbounds - for _, i := range c.InboundConfigs { - i.mu.RLock() +func snapshotInboundForSerialization(inbound *Inbound) *Inbound { + inbound.mu.RLock() + defer inbound.mu.RUnlock() + + snap := &Inbound{ + Tag: inbound.Tag, + Listen: inbound.Listen, + Port: inbound.Port, + Protocol: inbound.Protocol, + StreamSettings: inbound.StreamSettings, + Sniffing: inbound.Sniffing, + Allocation: inbound.Allocation, + exclude: inbound.exclude, } - // Build slices from maps for serialization - for _, i := range c.InboundConfigs { - if i.exclude { - continue - } + // Shallow copy: values share references with the original map. This is safe + // because Settings values are primitives or read-only config data (strings, + // ints, nested map[string]any from JSON unmarshal). If the data model ever + // adds mutable nested types (e.g., pointers to structs that get mutated), + // this loop must switch to a deep copy. + snap.Settings = make(map[string]any, len(inbound.Settings)+1) + for k, v := range inbound.Settings { + snap.Settings[k] = v + } - if i.Settings == nil { - i.Settings = make(map[string]any) - } + if inbound.exclude { + snap.Settings["clients"] = []any{} + return snap + } - if len(i.clients) == 0 { - i.Settings["clients"] = []any{} - continue + if len(inbound.clients) == 0 { + snap.Settings["clients"] = []any{} + return snap + } + + switch inbound.Protocol { + case Vmess: + clients := make([]*api.VmessAccount, 0, len(inbound.clients)) + for _, account := range inbound.clients { + if a, ok := account.(*api.VmessAccount); ok { + clients = append(clients, a) + } } + snap.Settings["clients"] = clients - switch i.Protocol { - case Vmess: - clients := make([]*api.VmessAccount, 0, len(i.clients)) - for _, account := range i.clients { - if vmessAccount, ok := account.(*api.VmessAccount); ok { - clients = append(clients, vmessAccount) - } + case Vless: + clients := make([]*api.VlessAccount, 0, len(inbound.clients)) + for _, account := range inbound.clients { + if a, ok := account.(*api.VlessAccount); ok { + clients = append(clients, a) } - i.Settings["clients"] = clients + } + snap.Settings["clients"] = clients - case Vless: - clients := make([]*api.VlessAccount, 0, len(i.clients)) - for _, account := range i.clients { - if vlessAccount, ok := account.(*api.VlessAccount); ok { - clients = append(clients, vlessAccount) - } + case Trojan: + clients := make([]*api.TrojanAccount, 0, len(inbound.clients)) + for _, account := range inbound.clients { + if a, ok := account.(*api.TrojanAccount); ok { + clients = append(clients, a) } - i.Settings["clients"] = clients + } + snap.Settings["clients"] = clients - case Trojan: - clients := make([]*api.TrojanAccount, 0, len(i.clients)) - for _, account := range i.clients { - if trojanAccount, ok := account.(*api.TrojanAccount); ok { - clients = append(clients, trojanAccount) + case Shadowsocks: + method, methodOk := inbound.Settings["method"].(string) + if methodOk && strings.HasPrefix(method, "2022-blake3") { + clients := make([]*api.ShadowsocksAccount, 0, len(inbound.clients)) + for _, account := range inbound.clients { + if a, ok := account.(*api.ShadowsocksAccount); ok { + clients = append(clients, a) } } - i.Settings["clients"] = clients - - case Shadowsocks: - method, methodOk := i.Settings["method"].(string) - if methodOk && strings.HasPrefix(method, "2022-blake3") { - clients := make([]*api.ShadowsocksAccount, 0, len(i.clients)) - for _, account := range i.clients { - if ssAccount, ok := account.(*api.ShadowsocksAccount); ok { - clients = append(clients, ssAccount) - } - } - i.Settings["clients"] = clients - } else { - clients := make([]*api.ShadowsocksTcpAccount, 0, len(i.clients)) - for _, account := range i.clients { - if ssTcpAccount, ok := account.(*api.ShadowsocksTcpAccount); ok { - clients = append(clients, ssTcpAccount) - } + snap.Settings["clients"] = clients + } else { + clients := make([]*api.ShadowsocksTcpAccount, 0, len(inbound.clients)) + for _, account := range inbound.clients { + if a, ok := account.(*api.ShadowsocksTcpAccount); ok { + clients = append(clients, a) } - i.Settings["clients"] = clients } + snap.Settings["clients"] = clients + } - case Hysteria: - clients := make([]*api.HysteriaAccount, 0, len(i.clients)) - for _, account := range i.clients { - if hyAccount, ok := account.(*api.HysteriaAccount); ok { - clients = append(clients, hyAccount) - } + case Hysteria: + clients := make([]*api.HysteriaAccount, 0, len(inbound.clients)) + for _, account := range inbound.clients { + if a, ok := account.(*api.HysteriaAccount); ok { + clients = append(clients, a) } - i.Settings["clients"] = clients } + snap.Settings["clients"] = clients } - // Save Variables for next use - aLog := c.LogConfig.AccessLog - eLog := c.LogConfig.ErrorLog - c.LogConfig.AccessLog = "" - c.LogConfig.ErrorLog = "" - - b, err := json.Marshal(c) - - // Restore variables to prevent conflict on next run - c.LogConfig.AccessLog = aLog - c.LogConfig.ErrorLog = eLog + return snap +} - // Release all locks - for _, i := range c.InboundConfigs { - i.mu.RUnlock() - } +func (c *Config) ToBytes() ([]byte, error) { + snapInbounds := make([]*Inbound, len(c.InboundConfigs)) + for idx, inbound := range c.InboundConfigs { + snapInbounds[idx] = snapshotInboundForSerialization(inbound) + } + + snapConfig := Config{ + RouterConfig: c.RouterConfig, + DNSConfig: c.DNSConfig, + InboundConfigs: snapInbounds, + OutboundConfigs: c.OutboundConfigs, + Policy: c.Policy, + API: c.API, + Metrics: c.Metrics, + Stats: c.Stats, + Reverse: c.Reverse, + FakeDNS: c.FakeDNS, + Observatory: c.Observatory, + BurstObservatory: c.BurstObservatory, + } + + if c.LogConfig != nil { + snapLog := *c.LogConfig + snapLog.AccessLog = "" + snapLog.ErrorLog = "" + snapConfig.LogConfig = &snapLog + } + + b, err := json.Marshal(&snapConfig) if err != nil { return nil, err diff --git a/backend/xray/core.go b/backend/xray/core.go index 7ae9b28..8cc4585 100644 --- a/backend/xray/core.go +++ b/backend/xray/core.go @@ -27,6 +27,7 @@ type Core struct { processPID int restarting bool stopping bool + cleanExit bool waitDone chan struct{} logsChan chan string logPhase uint32 @@ -202,10 +203,12 @@ func (c *Core) Start(xConfig *Config, debugMode bool) error { c.EnableStartupDiagnostics(c.startupLogSize) c.setStartupLogPhase() - // Clean up any orphaned xray processes before starting new one - if err := c.cleanupOrphanedProcesses(); err != nil { - log.Printf("warning: failed to cleanup orphaned processes: %v", err) + if !c.cleanExit { + if err := c.cleanupOrphanedProcesses(); err != nil { + log.Printf("warning: failed to cleanup orphaned processes: %v", err) + } } + c.cleanExit = false // Force kill any orphaned process in this Core instance before starting new one if c.process != nil && c.process.Process != nil { @@ -265,6 +268,9 @@ func (c *Core) Start(xConfig *Config, debugMode bool) error { func (c *Core) handleProcessExit(cmd *exec.Cmd, err error) { c.mu.Lock() expected := c.stopping || c.process != cmd + if expected { + c.cleanExit = true + } c.mu.Unlock() if expected { diff --git a/backend/xray/log.go b/backend/xray/log.go index 409d25d..cf9e4a9 100644 --- a/backend/xray/log.go +++ b/backend/xray/log.go @@ -5,13 +5,19 @@ import ( "context" "io" "regexp" + "sync" nodeLogger "github.com/pasarguard/node/logger" ) var ( - // Pattern for access logs: contains "accepted" (tcp/udp) and "email:" accessLogPattern = regexp.MustCompile(`from .+:\d+ accepted (tcp|udp):.+:\d+ \[.+\] email: .+`) + bufPool = sync.Pool{ + New: func() any { + buf := make([]byte, 64*1024) + return &buf + }, + } ) func (c *Core) detectLogType(log string) { @@ -19,23 +25,24 @@ func (c *Core) detectLogType(log string) { return } - // Check if it's an access log (contains accepted + email pattern) if accessLogPattern.MatchString(log) { c.logger.Log(nodeLogger.LogInfo, log) return } - // All other logs go to error file c.logger.Log(nodeLogger.LogError, log) } func (c *Core) captureProcessLogs(ctx context.Context, pipe io.Reader) { scanner := bufio.NewScanner(pipe) - scanner.Buffer(make([]byte, 64*1024), 1024*1024) + bufp := bufPool.Get().(*[]byte) + scanner.Buffer(*bufp, 1024*1024) + defer bufPool.Put(bufp) + for scanner.Scan() { select { case <-ctx.Done(): - return // Exit gracefully if stop signal received + return default: output := scanner.Text() if c.isStartupLogPhase() { @@ -58,24 +65,18 @@ func (c *Core) recordProcessLog(output string) { func (c *Core) captureStartupLogLine(output string) { c.RecordStartupLog(output) - // Non-blocking send: skip if channel is full to prevent deadlock select { case c.logsChan <- output: - // Log sent successfully default: - // Channel full, skip this log (prevents blocking xray process) } c.detectLogType(output) } func (c *Core) captureRuntimeLogLine(output string) { c.RecordRuntimeLog(output) - // Non-blocking send: skip if channel is full to prevent deadlock select { case c.logsChan <- output: - // Log sent successfully default: - // Channel full, skip this log (prevents blocking xray process) } c.detectLogType(output) } diff --git a/controller/controller.go b/controller/controller.go index b92e47f..1703ed1 100644 --- a/controller/controller.go +++ b/controller/controller.go @@ -37,12 +37,10 @@ type Controller struct { } func New(cfg *config.Config) *Controller { - _, cancel := context.WithCancel(context.Background()) return &Controller{ cfg: cfg, apiPort: netutil.FindFreePort(), metricPort: netutil.FindFreePort(), - cancelFunc: cancel, } } @@ -67,7 +65,14 @@ func (c *Controller) Connect(ip string, keepAlive uint64) { } func (c *Controller) Disconnect() { - c.cancelFunc() + c.mu.Lock() + cancel := c.cancelFunc + c.cancelFunc = nil + c.mu.Unlock() + + if cancel != nil { + cancel() + } c.mu.Lock() backend := c.backend diff --git a/pkg/sysstats/sys.go b/pkg/sysstats/sys.go index 7226016..3f37510 100644 --- a/pkg/sysstats/sys.go +++ b/pkg/sysstats/sys.go @@ -1,6 +1,7 @@ package sysstats import ( + "sync" "time" "github.com/shirou/gopsutil/v4/cpu" @@ -10,6 +11,18 @@ import ( "github.com/pasarguard/node/common" ) +type bandwidthState struct { + lastRxBytes uint64 + lastTxBytes uint64 + lastTime time.Time +} + +var ( + bwMu sync.Mutex + bwState bandwidthState + bwInitialized bool +) + func GetSystemStats() (*common.SystemStatsResponse, error) { stats := &common.SystemStatsResponse{} @@ -34,50 +47,58 @@ func GetSystemStats() (*common.SystemStatsResponse, error) { stats.CpuUsage = percentages[0] } - incomingSpeed, outgoingSpeed, err := getBandwidthSpeed() - if err != nil { - return stats, err - } + incomingSpeed, outgoingSpeed := getBandwidthSpeed() stats.IncomingBandwidthSpeed = incomingSpeed stats.OutgoingBandwidthSpeed = outgoingSpeed return stats, nil } -// getBandwidthSpeed returns the aggregate incoming (rx) and outgoing (tx) -// bandwidth in bytes per second, sampled over a 1-second interval. -// Loopback interface (lo) is excluded from the calculation. -func getBandwidthSpeed() (uint64, uint64, error) { - first, err := net.IOCounters(true) - if err != nil { - return 0, 0, err - } - - time.Sleep(1 * time.Second) - - second, err := net.IOCounters(true) +func getBandwidthSpeed() (uint64, uint64) { + counters, err := net.IOCounters(true) if err != nil { - return 0, 0, err + return 0, 0 } - prev := make(map[string]net.IOCountersStat, len(first)) - for _, c := range first { + now := time.Now() + var totalRx, totalTx uint64 + for _, c := range counters { if c.Name == "lo" { continue } - prev[c.Name] = c + totalRx += c.BytesRecv + totalTx += c.BytesSent } - var totalRxBytes, totalTxBytes uint64 - for _, c := range second { - if c.Name == "lo" { - continue - } - if p, ok := prev[c.Name]; ok { - totalRxBytes += c.BytesRecv - p.BytesRecv - totalTxBytes += c.BytesSent - p.BytesSent + bwMu.Lock() + defer bwMu.Unlock() + + if !bwInitialized { + bwState = bandwidthState{ + lastRxBytes: totalRx, + lastTxBytes: totalTx, + lastTime: now, } + bwInitialized = true + return 0, 0 + } + + elapsed := now.Sub(bwState.lastTime).Seconds() + if elapsed <= 0 { + return 0, 0 + } + + var rxSpeed, txSpeed uint64 + if totalRx >= bwState.lastRxBytes { + rxSpeed = uint64(float64(totalRx-bwState.lastRxBytes) / elapsed) } + if totalTx >= bwState.lastTxBytes { + txSpeed = uint64(float64(totalTx-bwState.lastTxBytes) / elapsed) + } + + bwState.lastRxBytes = totalRx + bwState.lastTxBytes = totalTx + bwState.lastTime = now - return totalRxBytes, totalTxBytes, nil + return rxSpeed, txSpeed }