Skip to content
181 changes: 105 additions & 76 deletions backend/xray/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 9 additions & 3 deletions backend/xray/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ type Core struct {
processPID int
restarting bool
stopping bool
cleanExit bool
waitDone chan struct{}
logsChan chan string
logPhase uint32
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
23 changes: 12 additions & 11 deletions backend/xray/log.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,37 +5,44 @@ 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) {
if c.logger == nil {
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() {
Expand All @@ -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)
}
11 changes: 8 additions & 3 deletions controller/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
}

Expand All @@ -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
Expand Down
Loading