Configuration System Overview
Xray's configuration system transforms human-readable JSON (or YAML/TOML) into protobuf messages that the core engine consumes. The pipeline has a clear separation: infra/conf/ handles parsing and validation, while core/ and app/ work exclusively with protobuf types.
Pipeline Architecture
flowchart LR
A[JSON/YAML/TOML File] --> B[Reader Decoder]
B --> C["conf.Config (Go struct)"]
C --> D["conf.Config.Build()"]
D --> E["core.Config (protobuf)"]
E --> F["core.New(config)"]
F --> G[Xray Instance]
subgraph "infra/conf/"
B
C
D
end
subgraph "core/"
E
F
G
endThe Top-Level Config Struct
File: infra/conf/xray.go
type Config struct {
Transport map[string]json.RawMessage `json:"transport"` // deprecated
LogConfig *LogConfig `json:"log"`
RouterConfig *RouterConfig `json:"routing"`
DNSConfig *DNSConfig `json:"dns"`
InboundConfigs []InboundDetourConfig `json:"inbounds"`
OutboundConfigs []OutboundDetourConfig `json:"outbounds"`
Policy *PolicyConfig `json:"policy"`
API *APIConfig `json:"api"`
Metrics *MetricsConfig `json:"metrics"`
Stats *StatsConfig `json:"stats"`
Reverse *ReverseConfig `json:"reverse"`
FakeDNS *FakeDNSConfig `json:"fakeDns"`
Observatory *ObservatoryConfig `json:"observatory"`
BurstObservatory *BurstObservatoryConfig `json:"burstObservatory"`
Version *VersionConfig `json:"version"`
}Each field corresponds to a top-level JSON key. The struct embeds sub-config types that know how to build their corresponding protobuf messages.
The Buildable Interface
File: infra/conf/buildable.go
type Buildable interface {
Build() (proto.Message, error)
}Every config struct implements this interface. The Build() method validates the config and produces the equivalent protobuf message.
Multi-Format Support
File: infra/conf/serial/loader.go
Xray supports JSON, YAML, and TOML configurations. All formats are converted to conf.Config via decoder functions:
var ReaderDecoderByFormat = map[string]readerDecoder{
"json": DecodeJSONConfig,
"yaml": DecodeYAMLConfig,
"toml": DecodeTOMLConfig,
}JSON: Parsed directly with a comment-stripping reader (json_reader.Reader), with syntax error position tracking.
YAML: Converted to JSON via yaml.YAMLToJSON(), then parsed as JSON.
TOML: Unmarshaled to map[string]interface{}, marshaled to JSON, then parsed as JSON.
Config Merging
File: infra/conf/serial/builder.go
Multiple config files can be merged:
func mergeConfigs(files []*core.ConfigSource) (*conf.Config, error) {
cf := &conf.Config{}
for i, file := range files {
c, _ := ReaderDecoderByFormat[file.Format](r)
if i == 0 {
*cf = *c
continue
}
cf.Override(c, file.Name)
}
return cf, nil
}The Override() method on Config handles merging:
- Simple fields (log, routing, DNS, policy, etc.) are replaced entirely
- Inbounds/outbounds are merged by tag: matching tags are updated, new tags are added
- Outbound ordering depends on the file name: files containing "tail" append, others prepend
The Build Pipeline
File: infra/conf/xray.go -- Config.Build()
The Build() method constructs the final core.Config:
func (c *Config) Build() (*core.Config, error) {
PostProcessConfigureFile(c) // lint and validation
config := &core.Config{
App: []*serial.TypedMessage{
serial.ToTypedMessage(&dispatcher.Config{}), // always present
serial.ToTypedMessage(&proxyman.InboundConfig{}), // always present
serial.ToTypedMessage(&proxyman.OutboundConfig{}),// always present
},
}
// Build and append each section
if c.API != nil { config.App = append(config.App, serial.ToTypedMessage(apiConf)) }
if c.Stats != nil { config.App = append(config.App, serial.ToTypedMessage(statsConf)) }
// Log is prepended (first to initialize)
config.App = append([]*serial.TypedMessage{logConfMsg}, config.App...)
if c.RouterConfig != nil { config.App = append(config.App, serial.ToTypedMessage(routerConfig)) }
if c.DNSConfig != nil { config.App = append(config.App, serial.ToTypedMessage(dnsApp)) }
if c.FakeDNS != nil { config.App = append([]*serial.TypedMessage{...}, config.App...) } // prepended!
// ... observatory, reverse, policy, version
// Build inbounds and outbounds
for _, rawInboundConfig := range inbounds {
ic, _ := rawInboundConfig.Build()
config.Inbound = append(config.Inbound, ic)
}
for _, rawOutboundConfig := range outbounds {
oc, _ := rawOutboundConfig.Build()
config.Outbound = append(config.Outbound, oc)
}
}Ordering matters:
- Log config is always first (so other modules can log during init)
- FakeDNS is prepended before DNS (must initialize first for
RequireFeatures) - Dispatcher, InboundConfig, OutboundConfig are always present as base apps
Inbound Config Building
type InboundDetourConfig struct {
Protocol string `json:"protocol"`
PortList *PortList `json:"port"`
ListenOn *Address `json:"listen"`
Settings *json.RawMessage `json:"settings"`
Tag string `json:"tag"`
StreamSetting *StreamConfig `json:"streamSettings"`
SniffingConfig *SniffingConfig `json:"sniffing"`
}The Build() method:
- Constructs
ReceiverConfig(listen address, port, stream settings, sniffing) - Loads protocol-specific settings via
inboundConfigLoader.LoadWithID() - Calls the loaded config's
Build()to get the protobuf proxy settings - Wraps both in
core.InboundHandlerConfigwithserial.ToTypedMessage()
Outbound Config Building
type OutboundDetourConfig struct {
Protocol string `json:"protocol"`
SendThrough *string `json:"sendThrough"`
Tag string `json:"tag"`
Settings *json.RawMessage `json:"settings"`
StreamSetting *StreamConfig `json:"streamSettings"`
ProxySettings *ProxyConfig `json:"proxySettings"`
MuxSettings *MuxConfig `json:"mux"`
TargetStrategy string `json:"targetStrategy"`
}The Build() method:
- Constructs
SenderConfig(via address, stream settings, mux, proxy chain) - Handles
targetStrategy(domain strategy for outbound DNS resolution) - Loads protocol-specific settings via
outboundConfigLoader.LoadWithID() - Wraps in
core.OutboundHandlerConfig
Protocol Config Loaders
File: infra/conf/xray.go
Two JSONConfigLoader registries map protocol names to config constructors:
var inboundConfigLoader = NewJSONConfigLoader(ConfigCreatorCache{
"dokodemo-door": func() interface{} { return new(DokodemoConfig) },
"http": func() interface{} { return new(HTTPServerConfig) },
"shadowsocks": func() interface{} { return new(ShadowsocksServerConfig) },
"socks": func() interface{} { return new(SocksServerConfig) },
"vless": func() interface{} { return new(VLessInboundConfig) },
"vmess": func() interface{} { return new(VMessInboundConfig) },
"trojan": func() interface{} { return new(TrojanServerConfig) },
// ...
}, "protocol", "settings")
var outboundConfigLoader = NewJSONConfigLoader(ConfigCreatorCache{
"freedom": func() interface{} { return new(FreedomConfig) },
"blackhole": func() interface{} { return new(BlackholeConfig) },
"vless": func() interface{} { return new(VLessOutboundConfig) },
"vmess": func() interface{} { return new(VMessOutboundConfig) },
"dns": func() interface{} { return new(DNSOutboundConfig) },
// ...
}, "protocol", "settings")LoadWithID() unmarshals the raw JSON settings into the appropriate config struct.
Key Source Files
| File | Purpose |
|---|---|
infra/conf/xray.go | Top-level Config, Build(), protocol loaders |
infra/conf/buildable.go | Buildable interface |
infra/conf/serial/builder.go | BuildConfig(), mergeConfigs() |
infra/conf/serial/loader.go | JSON/YAML/TOML decoders |
infra/conf/dns.go | DNSConfig, NameServerConfig |
infra/conf/router.go | RouterConfig |
infra/conf/api.go | APIConfig |
infra/conf/common.go | Shared types: Address, PortList, StringList |
core/config.go | core.Config protobuf, format registration |
Implementation Notes
The JSON reader (
infra/conf/json/reader.go) strips C-style comments (//and/* */) before parsing. This allows "JSONC" files.PostProcessConfigureFile()is called beforeBuild()to handle any pre-processing or validation. This is where lint checks run.The
Transportfield (global transport settings) is deprecated and returns an error if used, directing users to per-inbound/outboundstreamSettings.Protocol aliases exist:
"block"maps toBlackholeConfig,"direct"maps toFreedomConfig,"tunnel"maps toDokodemoConfig,"mixed"maps toSocksServerConfig.The
Override()method handles config merging for multi-file setups. When outbound configs have the same tag, the later file wins. New outbounds from non-"tail" files are prepended to maintain routing priority.The
BuildMPHCache()method onConfiggenerates a minimal perfect hash cache file for domain matchers, which can be loaded at startup to skip parsing geosite data files.