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.