Skip to content

Обзор системы конфигурации

Система конфигурации Xray преобразует человекочитаемый JSON (или YAML/TOML) в protobuf-сообщения, которые потребляет ядро. Конвейер имеет чёткое разделение: infra/conf/ отвечает за парсинг и валидацию, тогда как core/ и app/ работают исключительно с protobuf-типами.

Архитектура конвейера

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

Структура конфигурации верхнего уровня

Файл: infra/conf/xray.go

go
type Config struct {
    Transport       map[string]json.RawMessage `json:"transport"`  // устарело
    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"`
}

Каждое поле соответствует ключу верхнего уровня в JSON. Структура содержит вложенные типы конфигурации, которые знают, как построить соответствующие protobuf-сообщения.

Интерфейс Buildable

Файл: infra/conf/buildable.go

go
type Buildable interface {
    Build() (proto.Message, error)
}

Каждая структура конфигурации реализует этот интерфейс. Метод Build() валидирует конфигурацию и создаёт эквивалентное protobuf-сообщение.

Поддержка нескольких форматов

Файл: infra/conf/serial/loader.go

Xray поддерживает конфигурации в форматах JSON, YAML и TOML. Все форматы преобразуются в conf.Config через функции-декодеры:

go
var ReaderDecoderByFormat = map[string]readerDecoder{
    "json": DecodeJSONConfig,
    "yaml": DecodeYAMLConfig,
    "toml": DecodeTOMLConfig,
}

JSON: Парсится напрямую с ридером для удаления комментариев (json_reader.Reader), с отслеживанием позиции синтаксических ошибок.

YAML: Конвертируется в JSON через yaml.YAMLToJSON(), затем парсится как JSON.

TOML: Десериализуется в map[string]interface{}, сериализуется в JSON, затем парсится как JSON.

Объединение конфигураций

Файл: infra/conf/serial/builder.go

Несколько файлов конфигурации могут быть объединены:

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
}

Метод Override() структуры Config обрабатывает объединение:

  • Простые поля (log, routing, DNS, policy и т.д.) заменяются целиком
  • Inbound/outbound объединяются по тегу: совпадающие теги обновляются, новые теги добавляются
  • Порядок outbound зависит от имени файла: файлы, содержащие "tail", добавляются в конец, остальные — в начало

Конвейер сборки

Файл: infra/conf/xray.goConfig.Build()

Метод Build() создаёт финальный core.Config:

go
func (c *Config) Build() (*core.Config, error) {
    PostProcessConfigureFile(c)  // линтинг и валидация

    config := &core.Config{
        App: []*serial.TypedMessage{
            serial.ToTypedMessage(&dispatcher.Config{}),     // всегда присутствует
            serial.ToTypedMessage(&proxyman.InboundConfig{}), // всегда присутствует
            serial.ToTypedMessage(&proxyman.OutboundConfig{}),// всегда присутствует
        },
    }

    // Сборка и добавление каждой секции
    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 добавляется первым (для инициализации первым)
    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...) } // добавляется в начало!
    // ... observatory, reverse, policy, version

    // Сборка inbound и outbound
    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)
    }
}

Порядок имеет значение:

  1. Конфигурация логирования всегда первая (чтобы другие модули могли логировать при инициализации)
  2. FakeDNS добавляется перед DNS (должен инициализироваться первым для RequireFeatures)
  3. Dispatcher, InboundConfig, OutboundConfig всегда присутствуют как базовые приложения

Сборка конфигурации Inbound

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"`
}

Метод Build():

  1. Создаёт ReceiverConfig (адрес прослушивания, порт, настройки потока, сниффинг)
  2. Загружает протокол-специфичные настройки через inboundConfigLoader.LoadWithID()
  3. Вызывает Build() загруженной конфигурации для получения protobuf-настроек прокси
  4. Оборачивает оба в core.InboundHandlerConfig с serial.ToTypedMessage()

Сборка конфигурации Outbound

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"`
}

Метод Build():

  1. Создаёт SenderConfig (адрес via, настройки потока, mux, цепочка прокси)
  2. Обрабатывает targetStrategy (стратегия домена для DNS-разрешения на outbound)
  3. Загружает протокол-специфичные настройки через outboundConfigLoader.LoadWithID()
  4. Оборачивает в core.OutboundHandlerConfig

Загрузчики конфигураций протоколов

Файл: infra/conf/xray.go

Два реестра JSONConfigLoader сопоставляют имена протоколов с конструкторами конфигураций:

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() десериализует сырой JSON settings в соответствующую структуру конфигурации.

Ключевые исходные файлы

ФайлНазначение
infra/conf/xray.goВерхнеуровневый Config, Build(), загрузчики протоколов
infra/conf/buildable.goИнтерфейс Buildable
infra/conf/serial/builder.goBuildConfig(), mergeConfigs()
infra/conf/serial/loader.goДекодеры JSON/YAML/TOML
infra/conf/dns.goDNSConfig, NameServerConfig
infra/conf/router.goRouterConfig
infra/conf/api.goAPIConfig
infra/conf/common.goОбщие типы: Address, PortList, StringList
core/config.goprotobuf core.Config, регистрация форматов

Заметки по реализации

  • JSON-ридер (infra/conf/json/reader.go) удаляет комментарии в стиле C (// и /* */) перед парсингом. Это позволяет использовать файлы формата «JSONC».

  • PostProcessConfigureFile() вызывается перед Build() для предварительной обработки или валидации. Здесь выполняются проверки линтинга.

  • Поле Transport (глобальные настройки транспорта) устарело и возвращает ошибку при использовании, направляя пользователей к streamSettings для каждого inbound/outbound.

  • Существуют псевдонимы протоколов: "block" соответствует BlackholeConfig, "direct"FreedomConfig, "tunnel"DokodemoConfig, "mixed"SocksServerConfig.

  • Метод Override() обрабатывает объединение конфигураций при использовании нескольких файлов. Когда конфигурации outbound имеют одинаковый тег, более поздний файл имеет приоритет. Новые outbound из файлов без "tail" добавляются в начало для сохранения приоритета маршрутизации.

  • Метод BuildMPHCache() структуры Config генерирует файл кэша минимального идеального хэша для матчеров доменов, который может быть загружен при запуске для пропуска парсинга файлов данных geosite.

Технический анализ для целей повторной реализации.