Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions backend/xray/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (

"github.com/pasarguard/node/backend/xray/api"
"github.com/pasarguard/node/common"
nodeLogger "github.com/pasarguard/node/logger"

"github.com/xtls/xray-core/infra/conf"
)
Expand Down Expand Up @@ -556,8 +557,14 @@ func (c *Config) ToBytes() ([]byte, error) {

if c.LogConfig != nil {
snapLog := *c.LogConfig
snapLog.AccessLog = ""
snapLog.ErrorLog = ""
// The node reads Xray's stdout and writes the log files itself, so Xray gets no file
// paths. "none" is not a path: Xray uses it to turn that log off, so pass it through.
if snapLog.AccessLog != nodeLogger.DisabledLog {
snapLog.AccessLog = ""
}
if snapLog.ErrorLog != nodeLogger.DisabledLog {
snapLog.ErrorLog = ""
}
snapConfig.LogConfig = &snapLog
}

Expand Down
36 changes: 36 additions & 0 deletions backend/xray/config_tobytes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package xray
import (
"encoding/json"
"reflect"
"strings"
"sync"
"testing"

Expand Down Expand Up @@ -153,6 +154,41 @@ func TestToBytesDropsLogFilesWithoutChangingTheConfig(t *testing.T) {
}
}

// "none" turns a log off in Xray; clearing it like a file path would switch that log to stdout.
func TestToBytesKeepsDisabledLogs(t *testing.T) {
const fileLogs = `"access":"/var/log/xray/access.log","error":"/var/log/xray/error.log"`
tests := []struct {
name string
logs string
wantAccess string
wantError string
}{
{name: "both off", logs: `"access":"none","error":"none"`, wantAccess: "none", wantError: "none"},
{name: "access off, error to a file", logs: `"access":"none","error":"/var/log/xray/error.log"`, wantAccess: "none", wantError: ""},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
raw := strings.Replace(toBytesConfig, fileLogs, tt.logs, 1)
if raw == toBytesConfig {
t.Fatal("toBytesConfig no longer contains the log files this test replaces")
}
cfg, err := NewConfig(raw, []string{"static"})
if err != nil {
t.Fatal(err)
}
out := serializeConfig(t, cfg)

if access, _ := out.Log["access"].(string); access != tt.wantAccess {
t.Fatalf("serialized access log = %q, want %q", access, tt.wantAccess)
}
if errorLog, _ := out.Log["error"].(string); errorLog != tt.wantError {
t.Fatalf("serialized error log = %q, want %q", errorLog, tt.wantError)
}
})
}
}

func TestToBytesDoesNotWriteInboundSettings(t *testing.T) {
cfg := newToBytesConfig(t)
cfg.syncUsers(toBytesUsers())
Expand Down
13 changes: 13 additions & 0 deletions backend/xray/log.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,19 @@ func isAccessLog(log string) bool {
return strings.Contains(log, " accepted tcp:") || strings.Contains(log, " accepted udp:")
}

// isAccessLogEntry reports whether line is an Xray access-log entry, accepted or rejected:
// "[date time] from <source> accepted|rejected ...". It compares fields because Xray writes
// two spaces after "rejected" when the entry has no destination.
func isAccessLogEntry(line string) bool {
fields := strings.Fields(line)
for i := 0; i < len(fields) && i < 3; i++ {
if fields[i] == "from" {
return i+2 < len(fields) && (fields[i+2] == "accepted" || fields[i+2] == "rejected")
}
}
return false
}

func (c *Core) detectLogType(log string) {
if c.logger == nil {
return
Expand Down
22 changes: 22 additions & 0 deletions backend/xray/log_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,3 +111,25 @@ func TestDetectLogType(t *testing.T) {
})
}
}

func TestIsAccessLogEntry(t *testing.T) {
tests := []struct {
line string
want bool
}{
{line: "2026/11/04 07:27:52.690940 from 203.0.113.7:60253 accepted tcp:example.com:443 [in -> out] email: user1", want: true},
{line: "2026/11/04 07:27:52.692433 from tcp:203.0.113.8:8436 accepted udp:1.1.1.1:53 [in >> direct] email: user2", want: true},
// Rejected entries have no destination, so Xray writes two spaces after "rejected".
{line: "2026/11/04 07:30:41.120004 from 198.51.100.4:54596 rejected proxy/vless/encoding: invalid request user id: 00000000-0000-0000-0000-000000000000", want: true},
{line: "from 198.51.100.4:54596 rejected proxy/vless/encoding: invalid request user id", want: true},
{line: "2026/11/04 07:27:53.000001 [Warning] rejected request from 10.0.0.1:443: connection not accepted by destination", want: false},
{line: "Failed to start: app/proxyman/inbound: failed to listen TCP on 443", want: false},
{line: "", want: false},
}

for _, tt := range tests {
if got := isAccessLogEntry(tt.line); got != tt.want {
t.Fatalf("isAccessLogEntry(%q) = %v, want %v", tt.line, got, tt.want)
}
}
}
12 changes: 12 additions & 0 deletions backend/xray/startup_diagnostics.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,18 @@ func (c *Core) RuntimeLogTail(n int) []string {
}

func isStartupFailureLog(line string) bool {
// Access entries describe client connections. A destination such as
// "tcp:fatal-error.example:443" says nothing about whether Xray started.
if isAccessLogEntry(line) {
return false
}
// Info and Debug lines are per-connection details, e.g. "proxy/freedom: failed to open
// connection to ...", which appear as soon as clients reconnect. A failed start shows up
// as Xray's "Failed to start:" message, a panic, or a Warning/Error line.
if strings.Contains(line, "[Info]") || strings.Contains(line, "[Debug]") {
return false
}

lower := strings.ToLower(line)

// Client authentication/rejection logs can include "invalid" and are not startup-fatal.
Expand Down
8 changes: 8 additions & 0 deletions backend/xray/startup_diagnostics_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,14 @@ func TestIsStartupFailureLog(t *testing.T) {
{line: "2026/02/23 07:13:42.313765 from 5.250.114.189:0 rejected proxy/vless/encoding: invalid request user id: 1ffca7fc-4bb6-4701-9cfc-b046a28c569a", want: false},
{line: "accepted tcp:www.gstatic.com:443 [TAG -> DIRECT] email: a@b.com", want: false},
{line: "2026/02/21 [Info] app/stats: create new counter", want: false},
{line: "Failed to start: main: failed to load config files: [stdin:] > infra/conf: failed to build inbound config with tag in", want: true},
{line: "Failed to start: app/proxyman/inbound: failed to listen TCP on 443 > listen tcp 0.0.0.0:443: bind: address already in use", want: true},
// Clients reconnect while Xray starts. Their access entries and Info lines may contain
// any keyword; they must not fail the start.
{line: "2026/11/04 07:27:53.032627 from 203.0.113.7:57056 accepted tcp:fatal-error.example:443 [in >> direct] email: user1", want: false},
{line: "2026/11/04 07:27:53.040211 from tcp:203.0.113.8:8436 accepted udp:panic.example:53 [in >> direct] email: user2", want: false},
{line: "2026/11/04 07:27:53.051118 from 198.51.100.4:54596 rejected proxy/vless/encoding: invalid request user id: 00000000-0000-0000-0000-000000000000", want: false},
{line: "2026/11/04 04:48:07.229696 [Info] [3000105832] app/proxyman/outbound: app/proxyman/outbound: failed to process outbound traffic > proxy/freedom: failed to open connection to tcp:203.0.113.10:443 > dial tcp 203.0.113.10:443: connect: connection refused", want: false},
}

for _, tt := range tests {
Expand Down
6 changes: 5 additions & 1 deletion logger/logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ const (
LogCritical LogLevel = "Critical"
)

// DisabledLog is the value Xray accepts for "access" or "error" to turn that log off.
// It is not a file name.
const DisabledLog = "none"

type Logger struct {
outputLogs bool
accessLogFile *os.File
Expand All @@ -34,7 +38,7 @@ func New(outputLogs bool) *Logger {
}

func openLogFile(path string) (*os.File, error) {
if path == "" {
if path == "" || path == DisabledLog {
return nil, nil
}

Expand Down
23 changes: 23 additions & 0 deletions logger/logger_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package logger

import (
"os"
"testing"
)

func TestSetLogFileTreatsNoneAsDisabled(t *testing.T) {
t.Chdir(t.TempDir())

l := New(false)
if err := l.SetLogFile(DisabledLog, DisabledLog); err != nil {
t.Fatal(err)
}
defer l.Close()

l.Log(LogInfo, "from 203.0.113.7:51000 accepted tcp:example.com:443 [in >> direct] email: user1")
l.Log(LogError, "[Warning] core: Xray started")

if _, err := os.Stat(DisabledLog); !os.IsNotExist(err) {
t.Fatalf("SetLogFile(%q, %q) created a file named %q (stat error: %v)", DisabledLog, DisabledLog, DisabledLog, err)
}
}
Loading