Skip to content

JSON 到 Protobuf 的转换

本文描述 Xray 如何将人类可读的 JSON 配置转换为核心引擎使用的内部 Protobuf 表示。转换过程分为两个阶段:将 JSON 反序列化为中间 Go 结构体,然后通过 Build() 调用生成 Protobuf 消息。

总体流程

mermaid
flowchart TD
    A[配置文件] --> B{格式?}
    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["为每个 App 调用 CreateObject()"]
    K --> L[运行中的 Xray 实例]

阶段 1:JSON 到 Go 结构体

文件: infra/conf/serial/loader.go

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 结构体。inbound/outbound 配置上的 Settings 等子对象保留为 *json.RawMessage,用于延迟进行协议特定的解析。

阶段 2:Go 结构体到 Protobuf

conf.Config.Build() 方法协调整个转换过程。每个配置段遵循相同的模式:验证、构建 Protobuf、包装为 TypedMessage

TypedMessage 包装

包: common/serial

每个 Protobuf 消息在添加到 core.Config 之前都会被包装为 TypedMessage

go
type TypedMessage struct {
    Type  string  // 完整的 Protobuf 类型 URL
    Value []byte  // 序列化的 Protobuf 字节
}

func ToTypedMessage(message proto.Message) *TypedMessage {
    // 序列化消息并存储其类型 URL
}

这是允许在单个列表中存放异构配置对象的关键抽象。core.Config.App 字段是 []*TypedMessage,其中每个条目可以是任意 Protobuf 类型。

CreateObject:从 TypedMessage 到运行时对象

包: common

go
func CreateObject(ctx context.Context, config interface{}) (interface{}, error) {
    // 查找配置类型对应的已注册工厂
    // 调用工厂函数创建运行时对象
}

在实例启动期间,core.New() 遍历 config.App 并调用:

go
for _, appSettings := range config.App {
    obj, _ := CreateObject(ctx, appSettings)
    // 将对象注册为功能
}

工厂函数在各包的 init() 中注册:

go
// 示例: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:

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() 方法:

  1. 通过 parseDomainRule() 解析域名规则,处理 geosite:domain:full:regexp:keyword: 等前缀
  2. 通过 ToCidrList() 构建 expectedIPsunexpectedIPs
  3. 构建 dns.NameServer Protobuf,包含端点、域名规则和 GeoIP 匹配器
  4. 计算 policyID 用于并行查询分组

Inbound 配置

JSON:

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

构建过程:

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

json
{
    "outbounds": [{
        "protocol": "vless",
        "tag": "proxy",
        "settings": { "vnext": [...] },
        "streamSettings": { "network": "tcp" },
        "mux": { "enabled": true, "concurrency": 8 }
    }]
}

Go 结构体: conf.OutboundDetourConfig -> Protobuf: core.OutboundHandlerConfig

构建过程包括:

  1. SenderConfig 包含出口地址、流设置、mux 设置和代理链
  2. targetStrategy 映射到 internet.DomainStrategy 枚举
  3. 通过 outboundConfigLoader 加载协议特定设置

API 配置

JSON:

json
{
    "api": {
        "tag": "api",
        "listen": "127.0.0.1:10085",
        "services": ["HandlerService", "StatsService"]
    }
}

Go 结构体: conf.APIConfig -> Protobuf: commander.Config

文件: infra/conf/api.go

服务按名称映射到对应的 Protobuf 配置类型:

go
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 列表、端口范围、用户邮箱、协议名称和属性。每条规则的 Build() 创建一个 router.RoutingRule Protobuf。

core.Config Protobuf

文件: core/config.go

protobuf
message Config {
    repeated InboundHandlerConfig inbound = 1;
    repeated OutboundHandlerConfig outbound = 2;
    repeated TypedMessage app = 4;
}

App 列表是核心扩展点。任何功能(DNS、路由、统计、Observatory、反向代理、Commander)都作为 TypedMessage 添加到此列表中。核心在启动时遍历此列表并创建每个功能。

格式注册

文件: core/config.go

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

go
func init() {
    ReaderDecoderByFormat["json"] = DecodeJSONConfig
    ReaderDecoderByFormat["yaml"] = DecodeYAMLConfig
    ReaderDecoderByFormat["toml"] = DecodeTOMLConfig

    core.ConfigBuilderForFiles = BuildConfig
    core.ConfigMergedFormFiles = MergeConfigFromFiles
}

BuildConfig 函数将一切串联起来:

go
func BuildConfig(files []*core.ConfigSource) (*core.Config, error) {
    config, _ := mergeConfigs(files)
    return config.Build()
}

直接 Protobuf 加载

二进制 Protobuf 配置(.pb 文件)跳过 JSON 层直接加载:

go
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

go
// "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.PortRange 条目的 net.PortList

字符串列表

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 系统充当全局依赖注入容器。Protobuf 类型 URL 作为键,工厂函数生成运行时对象。

  • core.RequireFeatures() 允许延迟初始化:功能的工厂函数可以请求尚未创建的其他功能。核心在所有功能注册完成后解析这些依赖关系,触发延迟回调。

用于重新实现目的的技术分析。