JSON 到 Protobuf 的转换
本文描述 Xray 如何将人类可读的 JSON 配置转换为核心引擎使用的内部 Protobuf 表示。转换过程分为两个阶段:将 JSON 反序列化为中间 Go 结构体,然后通过 Build() 调用生成 Protobuf 消息。
总体流程
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
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:
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
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:等前缀 - 通过
ToCidrList()构建expectedIPs和unexpectedIPs - 构建
dns.NameServerProtobuf,包含端点、域名规则和 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包含出口地址、流设置、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 列表、端口范围、用户邮箱、协议名称和属性。每条规则的 Build() 创建一个 router.RoutingRule Protobuf。
core.Config Protobuf
文件: 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.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()允许延迟初始化:功能的工厂函数可以请求尚未创建的其他功能。核心在所有功能注册完成后解析这些依赖关系,触发延迟回调。