التحويل من 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]المرحلة الأولى: من 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 {
// Enhanced error reporting with line/char position
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 في إعدادات الوارد/الصادر تُحفظ كـ *json.RawMessage للتحليل المؤجل الخاص بالبروتوكول.
المرحلة الثانية: من بنى Go إلى Protobuf
تُنسّق دالة conf.Config.Build() عملية التحويل. كل قسم يتبع نفس النمط: التحقق، بناء protobuf، التغليف في TypedMessage.
تغليف TypedMessage
الحزمة: common/serial
كل رسالة protobuf تُغلَّف في TypedMessage قبل إضافتها إلى core.Config:
type TypedMessage struct {
Type string // full protobuf type URL
Value []byte // serialized protobuf bytes
}
func ToTypedMessage(message proto.Message) *TypedMessage {
// Marshals the message and stores its type URL
}هذا هو التجريد الأساسي الذي يسمح بكائنات إعدادات غير متجانسة في قائمة واحدة. حقل core.Config.App هو []*TypedMessage، حيث يمكن لكل إدخال أن يكون أي نوع protobuf.
CreateObject: من TypedMessage إلى كائن حي
الحزمة: common
func CreateObject(ctx context.Context, config interface{}) (interface{}, error) {
// Looks up the registered factory for the config's type
// Calls the factory function to create the runtime object
}أثناء بدء تشغيل النسخة، تتكرر core.New() على config.App وتستدعي:
for _, appSettings := range config.App {
obj, _ := CreateObject(ctx, appSettings)
// Register the object as a feature
}تم تسجيل المصنع في دالة init() لكل حزمة:
// Example from 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لتجميع الاستعلامات المتوازية
إعداد الوارد
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. Build ReceiverConfig (listen, port, stream, sniffing)
receiverSettings := &proxyman.ReceiverConfig{}
// 2. Load protocol-specific config
rawConfig, _ := inboundConfigLoader.LoadWithID(settings, c.Protocol)
// rawConfig is e.g. *VLessInboundConfig
// 3. Build protocol protobuf
ts, _ := rawConfig.(Buildable).Build()
// ts is e.g. *vless.ServerConfig (protobuf)
// 4. Wrap both in TypedMessage
return &core.InboundHandlerConfig{
Tag: c.Tag,
ReceiverSettings: serial.ToTypedMessage(receiverSettings),
ProxySettings: serial.ToTypedMessage(ts),
}, nil
}إعداد الصادر
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() لكل قاعدة تنشئ protobuf router.RoutingRule.
بنية core.Config Protobuf
الملف: core/config.go
message Config {
repeated InboundHandlerConfig inbound = 1;
repeated OutboundHandlerConfig outbound = 2;
repeated TypedMessage app = 4;
}قائمة App هي نقطة التوسع الأساسية. أي ميزة (DNS، التوجيه، الإحصائيات، المرصد، الوكيل العكسي، المتحكم) تُضاف هنا كـ 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()بالتهيئة المؤجلة: يمكن لمصنع ميزة أن يطلب ميزات أخرى لم تُنشأ بعد. يحل المحرك الأساسي هذه التبعيات بعد تسجيل جميع الميزات، مما يُفعّل الاستدعاءات المؤجلة.