From d5bc58dc6b7608e664515f970a9742e67e5f9623 Mon Sep 17 00:00:00 2001 From: yiguodev <147401898+yiguodev@users.noreply.github.com> Date: Fri, 10 Jul 2026 10:56:49 +0800 Subject: [PATCH] Root config: Add `env` config (#6400) https://github.com/XTLS/Xray-core/pull/6400#issuecomment-4880198100 https://github.com/XTLS/Xray-core/pull/6400#issuecomment-4916892425 https://github.com/XTLS/Xray-core/pull/6400#issuecomment-4929136670 --- .github/docker/Dockerfile | 1 + .github/docker/Dockerfile.usa | 1 + common/buf/io.go | 2 +- common/buf/readv_reader.go | 18 +++++-- common/buf/readv_reader_stub.go | 4 +- common/platform/env_reload.go | 40 ++++++++++++++ common/xudp/xudp.go | 52 +++++++++++------- core/xray.go | 3 ++ features/policy/policy.go | 26 ++++++--- infra/conf/env_test.go | 59 +++++++++++++++++++++ infra/conf/xray.go | 22 ++++++++ main/run.go | 4 ++ proxy/freedom/freedom.go | 24 ++++++--- proxy/vmess/outbound/outbound.go | 19 ++++--- transport/internet/browser_dialer/dialer.go | 18 +++++-- 15 files changed, 241 insertions(+), 52 deletions(-) create mode 100644 common/platform/env_reload.go create mode 100644 infra/conf/env_test.go diff --git a/.github/docker/Dockerfile b/.github/docker/Dockerfile index 2bb5de51d..e85706922 100644 --- a/.github/docker/Dockerfile +++ b/.github/docker/Dockerfile @@ -37,6 +37,7 @@ RUN echo '{}' >/tmp/usr/local/etc/xray/08_fakedns.json RUN echo '{}' >/tmp/usr/local/etc/xray/09_metrics.json RUN echo '{}' >/tmp/usr/local/etc/xray/10_observatory.json RUN echo '{}' >/tmp/usr/local/etc/xray/11_geodata.json +RUN echo '{}' >/tmp/usr/local/etc/xray/12_env.json RUN echo '{}' >/tmp/usr/local/etc/xray/99_version.json # Create log files diff --git a/.github/docker/Dockerfile.usa b/.github/docker/Dockerfile.usa index 432433b46..8aa096f8a 100644 --- a/.github/docker/Dockerfile.usa +++ b/.github/docker/Dockerfile.usa @@ -37,6 +37,7 @@ RUN echo '{}' >/tmp/usr/local/etc/xray/08_fakedns.json RUN echo '{}' >/tmp/usr/local/etc/xray/09_metrics.json RUN echo '{}' >/tmp/usr/local/etc/xray/10_observatory.json RUN echo '{}' >/tmp/usr/local/etc/xray/11_geodata.json +RUN echo '{}' >/tmp/usr/local/etc/xray/12_env.json RUN echo '{}' >/tmp/usr/local/etc/xray/99_version.json # Create log files diff --git a/common/buf/io.go b/common/buf/io.go index 75565e538..6b35052d2 100644 --- a/common/buf/io.go +++ b/common/buf/io.go @@ -122,7 +122,7 @@ func NewReader(reader io.Reader) Reader { } _, isFile := reader.(*os.File) - if !isFile && useReadv { + if !isFile && useReadV() { if sc, ok := reader.(syscall.Conn); ok { rawConn, err := sc.SyscallConn() if err != nil { diff --git a/common/buf/readv_reader.go b/common/buf/readv_reader.go index 7d7b3ead3..6ce44623e 100644 --- a/common/buf/readv_reader.go +++ b/common/buf/readv_reader.go @@ -5,6 +5,7 @@ package buf import ( "io" + "sync/atomic" "syscall" "github.com/xtls/xray-core/common/platform" @@ -143,13 +144,24 @@ func (r *ReadVReader) ReadMultiBuffer() (MultiBuffer, error) { return mb, nil } -var useReadv bool +var useReadv atomic.Bool -func init() { +func useReadV() bool { + return useReadv.Load() +} + +func reloadEnvSettings() error { const defaultFlagValue = "NOT_DEFINED_AT_ALL" value := platform.NewEnvFlag(platform.UseReadV).GetValue(func() string { return defaultFlagValue }) + enabled := false switch value { case defaultFlagValue, "auto", "enable": - useReadv = true + enabled = true } + useReadv.Store(enabled) + return nil +} + +func init() { + platform.RegisterEnvReload(reloadEnvSettings) } diff --git a/common/buf/readv_reader_stub.go b/common/buf/readv_reader_stub.go index b2be9825f..fcb6154f6 100644 --- a/common/buf/readv_reader_stub.go +++ b/common/buf/readv_reader_stub.go @@ -10,7 +10,9 @@ import ( "github.com/xtls/xray-core/features/stats" ) -const useReadv = false +func useReadV() bool { + return false +} func NewReadVReader(reader io.Reader, rawConn syscall.RawConn, counter stats.Counter) Reader { panic("not implemented") diff --git a/common/platform/env_reload.go b/common/platform/env_reload.go new file mode 100644 index 000000000..eafc884ef --- /dev/null +++ b/common/platform/env_reload.go @@ -0,0 +1,40 @@ +package platform + +import ( + "errors" + "sync" +) + +var envReloadRegistry = struct { + sync.RWMutex + handlers []func() error +}{} + +// RegisterEnvReload registers an environment reload handler and runs it once +// immediately so package defaults keep the same behavior as init-time reads. +func RegisterEnvReload(handler func() error) { + if handler == nil { + return + } + envReloadRegistry.Lock() + envReloadRegistry.handlers = append(envReloadRegistry.handlers, handler) + envReloadRegistry.Unlock() + if err := handler(); err != nil { + panic(err) + } +} + +// ReloadEnvSettings refreshes all registered environment-backed package state. +func ReloadEnvSettings() error { + envReloadRegistry.RLock() + handlers := append([]func() error{}, envReloadRegistry.handlers...) + envReloadRegistry.RUnlock() + + var errs []error + for _, handler := range handlers { + if err := handler(); err != nil { + errs = append(errs, err) + } + } + return errors.Join(errs...) +} diff --git a/common/xudp/xudp.go b/common/xudp/xudp.go index afaa602d2..30841b1e1 100644 --- a/common/xudp/xudp.go +++ b/common/xudp/xudp.go @@ -8,7 +8,7 @@ import ( "io" "strconv" "strings" - "time" + "sync/atomic" "github.com/xtls/xray-core/common/buf" "github.com/xtls/xray-core/common/errors" @@ -27,25 +27,39 @@ var AddrParser = protocol.NewAddressParser( ) var ( - Show bool - BaseKey []byte + Show atomic.Bool + baseKey atomic.Value ) -func init() { - if strings.ToLower(platform.NewEnvFlag(platform.XUDPLog).GetValue(func() string { return "" })) == "true" { - Show = true +func reloadEnvSettings() error { + Show.Store(strings.ToLower(platform.NewEnvFlag(platform.XUDPLog).GetValue(func() string { return "" })) == "true") + raw := platform.NewEnvFlag(platform.XUDPBaseKey).GetValue(func() string { return "" }) + if raw == "" { + ensureBaseKey() + return nil } - BaseKey = make([]byte, 32) - rand.Read(BaseKey) - go func() { - time.Sleep(100 * time.Millisecond) // this is not nice, but need to give some time for Android to setup ENV - if raw := platform.NewEnvFlag(platform.XUDPBaseKey).GetValue(func() string { return "" }); raw != "" { - if BaseKey, _ = base64.RawURLEncoding.DecodeString(raw); len(BaseKey) == 32 { - return - } - panic(platform.XUDPBaseKey + ": invalid value (BaseKey must be 32 bytes): " + raw + " len " + strconv.Itoa(len(BaseKey))) - } - }() + key, _ := base64.RawURLEncoding.DecodeString(raw) + if len(key) != 32 { + return errors.New(platform.XUDPBaseKey + ": invalid value (BaseKey must be 32 bytes): " + raw + " len " + strconv.Itoa(len(key))) + } + baseKey.Store(append([]byte(nil), key...)) + return nil +} + +func ensureBaseKey() []byte { + if key := baseKey.Load(); key != nil { + return key.([]byte) + } + key := make([]byte, 32) + if _, err := rand.Read(key); err != nil { + panic(err) + } + baseKey.Store(key) + return key +} + +func init() { + platform.RegisterEnvReload(reloadEnvSettings) } func GetGlobalID(ctx context.Context) (globalID [8]byte) { @@ -54,10 +68,10 @@ func GetGlobalID(ctx context.Context) (globalID [8]byte) { } if inbound := session.InboundFromContext(ctx); inbound != nil && inbound.Source.Network == net.Network_UDP && (inbound.Name == "dokodemo-door" || inbound.Name == "socks" || inbound.Name == "shadowsocks" || inbound.Name == "tun") { - h := blake3.New(8, BaseKey) + h := blake3.New(8, ensureBaseKey()) h.Write([]byte(inbound.Source.String())) copy(globalID[:], h.Sum(nil)) - if Show { + if Show.Load() { errors.LogInfo(ctx, fmt.Sprintf("XUDP inbound.Source.String(): %v\tglobalID: %v\n", inbound.Source.String(), globalID)) } } diff --git a/core/xray.go b/core/xray.go index 58135c96b..a70507068 100644 --- a/core/xray.go +++ b/core/xray.go @@ -187,6 +187,9 @@ func NewWithContext(ctx context.Context, config *Config) (*Instance, error) { } func initInstanceWithConfig(config *Config, server *Instance) (bool, error) { + if err := platform.ReloadEnvSettings(); err != nil { + return true, errors.New("failed to reload environment settings").Base(err) + } server.ctx = context.WithValue(server.ctx, "cone", platform.NewEnvFlag(platform.UseCone).GetValue(func() string { return "" }) != "true") diff --git a/features/policy/policy.go b/features/policy/policy.go index d6fd20d06..cfbd6207b 100644 --- a/features/policy/policy.go +++ b/features/policy/policy.go @@ -3,6 +3,7 @@ package policy import ( "context" "runtime" + "sync/atomic" "time" "github.com/xtls/xray-core/common/platform" @@ -82,32 +83,41 @@ func ManagerType() interface{} { return (*Manager)(nil) } -var defaultBufferSize int32 +var defaultBufferSize atomic.Int32 -func init() { +func reloadEnvSettings() error { + defaultBufferSize.Store(readDefaultBufferSize()) + return nil +} + +func readDefaultBufferSize() int32 { const defaultValue = -17 size := platform.NewEnvFlag(platform.BufferSize).GetValueAsInt(defaultValue) switch size { case 0: - defaultBufferSize = -1 // For pipe to use unlimited size + return -1 // For pipe to use unlimited size case defaultValue: // Env flag not defined. Use default values per CPU-arch. switch runtime.GOARCH { case "arm", "mips", "mipsle": - defaultBufferSize = 0 + return 0 case "arm64", "mips64", "mips64le": - defaultBufferSize = 4 * 1024 // 4k cache for low-end devices + return 4 * 1024 // 4k cache for low-end devices default: - defaultBufferSize = 512 * 1024 + return 512 * 1024 } default: - defaultBufferSize = int32(size) * 1024 * 1024 + return int32(size) * 1024 * 1024 } } +func init() { + platform.RegisterEnvReload(reloadEnvSettings) +} + func defaultBufferPolicy() Buffer { return Buffer{ - PerConnection: defaultBufferSize, + PerConnection: defaultBufferSize.Load(), } } diff --git a/infra/conf/env_test.go b/infra/conf/env_test.go new file mode 100644 index 000000000..ae499a5ee --- /dev/null +++ b/infra/conf/env_test.go @@ -0,0 +1,59 @@ +package conf + +import ( + "encoding/json" + "os" + "testing" +) + +func TestRootEnvAppliesArbitraryValues(t *testing.T) { + const ( + valueKey = "XRAY_TEST_CONFIG_ENV" + emptyKey = "XRAY_TEST_CONFIG_EMPTY" + ) + t.Setenv(valueKey, "before") + t.Setenv(emptyKey, "before") + + config := new(Config) + if err := json.Unmarshal([]byte(`{ + "env": { + "XRAY_TEST_CONFIG_ENV": "configured", + "XRAY_TEST_CONFIG_EMPTY": "" + } + }`), config); err != nil { + t.Fatal(err) + } + if _, err := config.Build(); err != nil { + t.Fatal(err) + } + + if got := os.Getenv(valueKey); got != "configured" { + t.Fatalf("env %q = %q, want %q", valueKey, got, "configured") + } + if got := os.Getenv(emptyKey); got != "" { + t.Fatalf("env %q = %q, want empty", emptyKey, got) + } +} + +func TestEnvConfigOverride(t *testing.T) { + base := EnvConfig{ + "ONE": "one", + "TWO": "old", + } + override := EnvConfig{ + "TWO": "new", + "THREE": "three", + } + base.Override(override) + + want := map[string]string{ + "ONE": "one", + "TWO": "new", + "THREE": "three", + } + for key, value := range want { + if got := base[key]; got != value { + t.Fatalf("env %q = %q, want %q", key, got, value) + } + } +} diff --git a/infra/conf/xray.go b/infra/conf/xray.go index 91d161183..a0846a817 100644 --- a/infra/conf/xray.go +++ b/infra/conf/xray.go @@ -3,6 +3,7 @@ package conf import ( "context" "encoding/json" + "os" "path/filepath" "strings" @@ -379,11 +380,20 @@ func (c *StatsConfig) Build() (*stats.Config, error) { return &stats.Config{}, nil } +type EnvConfig map[string]string + +func (c EnvConfig) Override(o EnvConfig) { + for key, value := range o { + c[key] = value + } +} + type Config struct { // Deprecated: Global transport config is no longer used // left for returning error Transport map[string]json.RawMessage `json:"transport"` + Env EnvConfig `json:"env"` LogConfig *LogConfig `json:"log"` RouterConfig *RouterConfig `json:"routing"` DNSConfig *DNSConfig `json:"dns"` @@ -439,6 +449,12 @@ func (c *Config) Override(o *Config, fn string) { if o.Transport != nil { c.Transport = o.Transport } + if o.Env != nil { + if c.Env == nil { + c.Env = EnvConfig{} + } + c.Env.Override(o.Env) + } if o.Policy != nil { c.Policy = o.Policy } @@ -514,6 +530,12 @@ func (c *Config) Override(o *Config, fn string) { // Build implements Buildable. func (c *Config) Build() (*core.Config, error) { + for key, value := range c.Env { + if err := os.Setenv(key, value); err != nil { + return nil, errors.New("failed to apply environment configuration").Base(err) + } + } + if err := PostProcessConfigureFile(c); err != nil { return nil, errors.New("failed to post-process configuration file").Base(err) } diff --git a/main/run.go b/main/run.go index 2655f7833..b87ca6356 100644 --- a/main/run.go +++ b/main/run.go @@ -36,6 +36,10 @@ The -confdir=dir flag sets a dir with multiple json config The -format=json flag sets the format of config files. Default "auto". +The config root env object sets process environment variables after all config +files are parsed. Variables needed to locate or parse config files must be set +in the process environment before Xray starts. + The -test flag tells Xray to test config files only, without launching the server. diff --git a/proxy/freedom/freedom.go b/proxy/freedom/freedom.go index 51c90a7ab..00ed3f4ca 100644 --- a/proxy/freedom/freedom.go +++ b/proxy/freedom/freedom.go @@ -5,6 +5,7 @@ import ( "crypto/rand" "io" "strings" + "sync/atomic" "time" "github.com/pires/go-proxyproto" @@ -31,12 +32,24 @@ import ( ) var ( - useSplice bool + useSplice atomic.Bool allNetworks [8]bool defaultBlockPrivateRule *FinalRule defaultBlockAllRule *FinalRule ) +func reloadEnvSettings() error { + const defaultFlagValue = "NOT_DEFINED_AT_ALL" + value := platform.NewEnvFlag(platform.UseFreedomSplice).GetValue(func() string { return defaultFlagValue }) + enabled := false + switch value { + case defaultFlagValue, "auto", "enable": + enabled = true + } + useSplice.Store(enabled) + return nil +} + func init() { common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { h := new(Handler) @@ -48,12 +61,7 @@ func init() { return h, nil })) - const defaultFlagValue = "NOT_DEFINED_AT_ALL" - value := platform.NewEnvFlag(platform.UseFreedomSplice).GetValue(func() string { return defaultFlagValue }) - switch value { - case defaultFlagValue, "auto", "enable": - useSplice = true - } + platform.RegisterEnvReload(reloadEnvSettings) for i := range allNetworks { allNetworks[i] = true @@ -422,7 +430,7 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte responseDone := func() error { defer timer.SetTimeout(plcy.Timeouts.UplinkOnly) - if destination.Network == net.Network_TCP && useSplice && proxy.IsRAWTransportWithoutSecurity(conn) { // it would be tls conn in special use case of MITM, we need to let link handle traffic + if destination.Network == net.Network_TCP && useSplice.Load() && proxy.IsRAWTransportWithoutSecurity(conn) { // it would be tls conn in special use case of MITM, we need to let link handle traffic var writeConn net.Conn var inTimer *signal.ActivityTimer if inbound := session.InboundFromContext(ctx); inbound != nil && inbound.Conn != nil { diff --git a/proxy/vmess/outbound/outbound.go b/proxy/vmess/outbound/outbound.go index bdc3ecc25..db5f94abb 100644 --- a/proxy/vmess/outbound/outbound.go +++ b/proxy/vmess/outbound/outbound.go @@ -5,6 +5,7 @@ import ( "crypto/hmac" "crypto/sha256" "hash/crc64" + "sync/atomic" "time" "github.com/xtls/xray-core/common" @@ -218,10 +219,17 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte return nil } -var enablePadding = false +var enablePadding atomic.Bool func shouldEnablePadding(s protocol.SecurityType) bool { - return enablePadding || s == protocol.SecurityType_AES128_GCM || s == protocol.SecurityType_CHACHA20_POLY1305 || s == protocol.SecurityType_AUTO + return enablePadding.Load() || s == protocol.SecurityType_AES128_GCM || s == protocol.SecurityType_CHACHA20_POLY1305 || s == protocol.SecurityType_AUTO +} + +func reloadEnvSettings() error { + const defaultFlagValue = "NOT_DEFINED_AT_ALL" + paddingValue := platform.NewEnvFlag(platform.UseVmessPadding).GetValue(func() string { return defaultFlagValue }) + enablePadding.Store(paddingValue != defaultFlagValue) + return nil } func init() { @@ -229,10 +237,5 @@ func init() { return New(ctx, config.(*Config)) })) - const defaultFlagValue = "NOT_DEFINED_AT_ALL" - - paddingValue := platform.NewEnvFlag(platform.UseVmessPadding).GetValue(func() string { return defaultFlagValue }) - if paddingValue != defaultFlagValue { - enablePadding = true - } + platform.RegisterEnvReload(reloadEnvSettings) } diff --git a/transport/internet/browser_dialer/dialer.go b/transport/internet/browser_dialer/dialer.go index 5705f9503..ee3828020 100644 --- a/transport/internet/browser_dialer/dialer.go +++ b/transport/internet/browser_dialer/dialer.go @@ -27,9 +27,10 @@ type task struct { } var ( - conns chan *websocket.Conn - server *http.Server - mu sync.Mutex + conns chan *websocket.Conn + server *http.Server + currentAddr string + mu sync.Mutex ) var upgrader = &websocket.Upgrader{ @@ -47,8 +48,13 @@ func Reload() { mu.Lock() defer mu.Unlock() + if addr == currentAddr && (addr == "" || server != nil) { + return + } + if server != nil { server.Close() + server = nil } if HasBrowserDialer() { for len(conns) > 0 { @@ -60,6 +66,7 @@ func Reload() { } conns = nil } + currentAddr = addr if addr != "" { token := uuid.New() csrfToken := token.String() @@ -220,5 +227,8 @@ func CheckOK(conn *websocket.Conn) error { } func init() { - Reload() + platform.RegisterEnvReload(func() error { + Reload() + return nil + }) }