Преобразование JSON в Protobuf
Этот документ описывает, как Xray преобразует человекочитаемую JSON-конфигурацию во внутреннее protobuf-представление, используемое ядром. Преобразование — это двухфазный процесс: десериализация JSON в промежуточные Go-структуры, за которой следуют вызовы Build(), создающие protobuf-сообщения.
Общий поток
flowchart TD
A[Config file] --> B{Format?}
B -->|JSON| C[DecodeJSONConfig]
B -->|YAML| D[DecodeYAMLConfig]
B -->|TOML| E[DecodeTOMLConfig]
B -->|Protobuf| F[loadProtobufConfig]
C --> G["conf.Config{}"]
D -->|yaml->json| C
E -->|toml->map->json| C
F --> H["core.Config{}"]
G --> I["conf.Config.Build()"]
I --> H
H --> J["core.New(config)"]
J --> K["CreateObject() for each App"]
K --> L[Running Xray Instance]Фаза 1: JSON в Go-структуры
Файл: infra/conf/serial/loader.go
func DecodeJSONConfig(reader io.Reader) (*conf.Config, error) {
jsonConfig := &conf.Config{}
jsonReader := io.TeeReader(&json_reader.Reader{Reader: reader}, jsonContent)
decoder := json.NewDecoder(jsonReader)
if err := decoder.Decode(jsonConfig); err != nil {
// Расширенные сообщения об ошибках с позицией строки/символа
var pos *offset
switch tErr := cause.(type) {
case *json.SyntaxError:
pos = findOffset(jsonContent.Bytes(), int(tErr.Offset))
case *json.UnmarshalTypeError:
pos = findOffset(jsonContent.Bytes(), int(tErr.Offset))
}
}
return jsonConfig, nil
}JSON-ридер сначала удаляет комментарии, затем стандартный декодер encoding/json заполняет структуру conf.Config. Подобъекты, такие как Settings в конфигурациях inbound/outbound, сохраняются как *json.RawMessage для отложенного протокол-специфичного парсинга.
Фаза 2: Go-структуры в Protobuf
Метод conf.Config.Build() оркестрирует преобразование. Каждая секция следует одному и тому же шаблону: валидация, построение protobuf, обёртка в TypedMessage.
Обёртка TypedMessage
Пакет: common/serial
Каждое protobuf-сообщение оборачивается в TypedMessage перед добавлением в core.Config:
type TypedMessage struct {
Type string // полный URL типа protobuf
Value []byte // сериализованные байты protobuf
}
func ToTypedMessage(message proto.Message) *TypedMessage {
// Сериализует сообщение и сохраняет его URL типа
}Это ключевая абстракция, позволяющая размещать разнородные объекты конфигурации в одном списке. Поле core.Config.App имеет тип []*TypedMessage, где каждая запись может быть любым protobuf-типом.
CreateObject: из TypedMessage в живой объект
Пакет: common
func CreateObject(ctx context.Context, config interface{}) (interface{}, error) {
// Ищет зарегистрированную фабрику для типа конфигурации
// Вызывает фабричную функцию для создания объекта времени выполнения
}При запуске экземпляра core.New() перебирает config.App и вызывает:
for _, appSettings := range config.App {
obj, _ := CreateObject(ctx, appSettings)
// Регистрация объекта как функционального модуля
}Фабрика была зарегистрирована в init() каждого пакета:
// Пример из app/dns/dns.go
func init() {
common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) {
return New(ctx, config.(*Config))
}))
}Посекционное сопоставление
Конфигурация DNS
JSON:
{
"dns": {
"servers": [
{ "address": "8.8.8.8", "port": 53, "domains": ["geosite:google"] },
"1.1.1.1"
],
"hosts": { "example.com": "1.2.3.4" },
"queryStrategy": "UseIP",
"disableCache": false
}
}Go-структура: conf.DNSConfig -> Protobuf: dns.Config
Файл: infra/conf/dns.go
NameServerConfig имеет пользовательский UnmarshalJSON, принимающий как простую строку ("1.1.1.1"), так и полный объект. Метод Build():
- Парсит правила доменов через
parseDomainRule(), обрабатывая префиксыgeosite:,domain:,full:,regexp:,keyword: - Строит
expectedIPsиunexpectedIPsчерезToCidrList() - Создаёт protobuf
dns.NameServerс эндпоинтом, правилами доменов и матчерами GeoIP - Вычисляет
policyIDдля группировки параллельных запросов
Конфигурация Inbound
JSON:
{
"inbounds": [{
"protocol": "vless",
"port": 443,
"listen": "0.0.0.0",
"settings": { "clients": [...] },
"streamSettings": { "network": "tcp", "security": "tls" },
"sniffing": { "enabled": true, "destOverride": ["http", "tls"] }
}]
}Go-структура: conf.InboundDetourConfig -> Protobuf: core.InboundHandlerConfig
Процесс сборки:
func (c *InboundDetourConfig) Build() (*core.InboundHandlerConfig, error) {
// 1. Построение ReceiverConfig (прослушивание, порт, поток, сниффинг)
receiverSettings := &proxyman.ReceiverConfig{}
// 2. Загрузка протокол-специфичной конфигурации
rawConfig, _ := inboundConfigLoader.LoadWithID(settings, c.Protocol)
// rawConfig — это, например, *VLessInboundConfig
// 3. Построение protobuf протокола
ts, _ := rawConfig.(Buildable).Build()
// ts — это, например, *vless.ServerConfig (protobuf)
// 4. Обёртка обоих в TypedMessage
return &core.InboundHandlerConfig{
Tag: c.Tag,
ReceiverSettings: serial.ToTypedMessage(receiverSettings),
ProxySettings: serial.ToTypedMessage(ts),
}, nil
}Конфигурация Outbound
JSON:
{
"outbounds": [{
"protocol": "vless",
"tag": "proxy",
"settings": { "vnext": [...] },
"streamSettings": { "network": "tcp" },
"mux": { "enabled": true, "concurrency": 8 }
}]
}Go-структура: conf.OutboundDetourConfig -> Protobuf: core.OutboundHandlerConfig
Процесс сборки включает:
SenderConfigс адресом via, настройками потока, настройками mux и цепочкой проксиtargetStrategyсопоставляется с перечислениемinternet.DomainStrategy- Протокол-специфичные настройки загружаются через
outboundConfigLoader
Конфигурация API
JSON:
{
"api": {
"tag": "api",
"listen": "127.0.0.1:10085",
"services": ["HandlerService", "StatsService"]
}
}Go-структура: conf.APIConfig -> Protobuf: commander.Config
Файл: infra/conf/api.go
Сервисы сопоставляются по имени с их protobuf-типами конфигурации:
func (c *APIConfig) Build() (*commander.Config, error) {
services := make([]*serial.TypedMessage, 0)
for _, s := range c.Services {
switch strings.ToLower(s) {
case "handlerservice":
services = append(services, serial.ToTypedMessage(&handlerservice.Config{}))
case "statsservice":
services = append(services, serial.ToTypedMessage(&statsservice.Config{}))
// ...
}
}
return &commander.Config{Tag: c.Tag, Listen: c.Listen, Service: services}, nil
}Конфигурация маршрутизации
Go-структура: conf.RouterConfig -> Protobuf: router.Config
Правила маршрутизации парсятся из JSON с поддержкой списков доменов, списков IP, диапазонов портов, email пользователей, имён протоколов и атрибутов. Метод Build() каждого правила создаёт protobuf router.RoutingRule.
Protobuf core.Config
Файл: core/config.go
message Config {
repeated InboundHandlerConfig inbound = 1;
repeated OutboundHandlerConfig outbound = 2;
repeated TypedMessage app = 4;
}Список App — это основная точка расширения. Любой функциональный модуль (DNS, маршрутизация, статистика, Observatory, обратный прокси, Commander) добавляется сюда как TypedMessage. Ядро перебирает этот список при запуске и создаёт каждый модуль.
Регистрация форматов
Файл: core/config.go
type ConfigFormat struct {
Name string
Extension []string
Loader ConfigLoader
}
func RegisterConfigLoader(format *ConfigFormat) error {
configLoaderByName[name] = format
for _, ext := range format.Extension {
configLoaderByExt[ext] = format
}
}Файл: infra/conf/serial/builder.go
func init() {
ReaderDecoderByFormat["json"] = DecodeJSONConfig
ReaderDecoderByFormat["yaml"] = DecodeYAMLConfig
ReaderDecoderByFormat["toml"] = DecodeTOMLConfig
core.ConfigBuilderForFiles = BuildConfig
core.ConfigMergedFormFiles = MergeConfigFromFiles
}Функция BuildConfig связывает всё вместе:
func BuildConfig(files []*core.ConfigSource) (*core.Config, error) {
config, _ := mergeConfigs(files)
return config.Build()
}Прямая загрузка Protobuf
Бинарные protobuf-конфигурации (файлы .pb) полностью пропускают слой JSON:
func loadProtobufConfig(data []byte) (*Config, error) {
config := new(Config)
proto.Unmarshal(data, config)
return config, nil
}Допускается только один protobuf-файл (объединение невозможно), и он должен содержать полный core.Config.
Ключевые шаблоны преобразования
Типы адресов
Тип conf.Address обрабатывает IP-адреса, домены и специальные значения. Его метод Build() создаёт net.IPOrDomain:
// "1.2.3.4" -> net.IPOrDomain{Address: &IPOrDomain_Ip{Ip: [4]byte}}
// "example.com" -> net.IPOrDomain{Address: &IPOrDomain_Domain{Domain: "example.com"}}Списки портов
conf.PortList парсит диапазоны вида "1024-65535" или перечисления через запятую "80,443,8080" и создаёт net.PortList с записями net.PortRange.
Списки строк
conf.StringList — это []string с пользовательской JSON-десериализацией, принимающей как одну строку, так и массив строк.
Правила доменов
Строки доменов парсятся с определением префикса:
"domain:example.com"-> Сопоставление по поддомену"full:example.com"-> Точное сопоставление домена"regexp:.*\\.example\\.com"-> Сопоставление по регулярному выражению"keyword:example"-> Сопоставление по ключевому слову"geosite:category"-> Внешний список geosite"ext:filename:list"-> Список доменов из внешнего файла- Без префикса -> Обрабатывается как поддомен или geosite (зависит от контекста)
Заметки по реализации
Преобразование JSON в protobuf строго однонаправленное. Обратного преобразования работающего
core.Configв JSON не предусмотрено (хотяMergeConfigFromFilesможет создавать объединённый JSON для отображения с использованием рефлексии).Шаблон
json.RawMessageдля настроек протоколов критически важен: он позволяет парсить внешнюю конфигурацию без знания типа протокола, а затем внутренние настройки парсятся, когда протокол уже известен.Сообщения об ошибках из
Build()включают контекст о том, какая секция вызвала ошибку (например, "failed to build DNS configuration"), что упрощает отладку конфигурации.Пути YAML и TOML в конечном счёте создают JSON и передают его в
DecodeJSONConfig. Это означает, что все YAML/TOML-конфигурации должны быть представимы в виде валидных JSON-структур. Сложные возможности YAML (якоря, мультидокументы) могут не работать.Система
common.RegisterConfig/common.CreateObjectдействует как глобальный контейнер внедрения зависимостей. URL типов protobuf служат ключами, а фабричные функции создают объекты времени выполнения.core.RequireFeatures()обеспечивает отложенную инициализацию: фабрика функционального модуля может запросить другие модули, которые ещё не были созданы. Ядро разрешает эти зависимости после регистрации всех модулей, вызывая отложенные callback-функции.