Skip to content

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 ​

mermaid
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
    end

The Top-Level Config Struct ​

File: infra/conf/xray.go

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

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:

go
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:

go
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:

go
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:

  1. Log config is always first (so other modules can log during init)
  2. FakeDNS is prepended before DNS (must initialize first for RequireFeatures)
  3. Dispatcher, InboundConfig, OutboundConfig are always present as base apps

Inbound Config Building ​

go
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:

  1. Constructs ReceiverConfig (listen address, port, stream settings, sniffing)
  2. Loads protocol-specific settings via inboundConfigLoader.LoadWithID()
  3. Calls the loaded config's Build() to get the protobuf proxy settings
  4. Wraps both in core.InboundHandlerConfig with serial.ToTypedMessage()

Outbound Config Building ​

go
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:

  1. Constructs SenderConfig (via address, stream settings, mux, proxy chain)
  2. Handles targetStrategy (domain strategy for outbound DNS resolution)
  3. Loads protocol-specific settings via outboundConfigLoader.LoadWithID()
  4. Wraps in core.OutboundHandlerConfig

Protocol Config Loaders ​

File: infra/conf/xray.go

Two JSONConfigLoader registries map protocol names to config constructors:

go
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 ​

FilePurpose
infra/conf/xray.goTop-level Config, Build(), protocol loaders
infra/conf/buildable.goBuildable interface
infra/conf/serial/builder.goBuildConfig(), mergeConfigs()
infra/conf/serial/loader.goJSON/YAML/TOML decoders
infra/conf/dns.goDNSConfig, NameServerConfig
infra/conf/router.goRouterConfig
infra/conf/api.goAPIConfig
infra/conf/common.goShared types: Address, PortList, StringList
core/config.gocore.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 before Build() to handle any pre-processing or validation. This is where lint checks run.

  • The Transport field (global transport settings) is deprecated and returns an error if used, directing users to per-inbound/outbound streamSettings.

  • Protocol aliases exist: "block" maps to BlackholeConfig, "direct" maps to FreedomConfig, "tunnel" maps to DokodemoConfig, "mixed" maps to SocksServerConfig.

  • 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 on Config generates a minimal perfect hash cache file for domain matchers, which can be loaded at startup to skip parsing geosite data files.

Technical analysis for re-implementation purposes.