Skip to content

التحويل من JSON إلى Protobuf

يصف هذا المستند كيفية تحويل Xray لإعدادات JSON القابلة للقراءة البشرية إلى تمثيل protobuf الداخلي المُستخدم من المحرك الأساسي. التحويل هو عملية من مرحلتين: فك تسلسل JSON إلى بنى Go وسيطة، ثم استدعاءات Build() التي تُنتج رسائل protobuf.

التدفق العام

mermaid
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

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:

go
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

go
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 وتستدعي:

go
for _, appSettings := range config.App {
    obj, _ := CreateObject(ctx, appSettings)
    // Register the object as a feature
}

تم تسجيل المصنع في دالة init() لكل حزمة:

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

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. بناء expectedIPs و unexpectedIPs عبر ToCidrList()
  3. إنشاء protobuf dns.NameServer مع نقطة النهاية وقواعد النطاقات ومُطابقات GeoIP
  4. حساب policyID لتجميع الاستعلامات المتوازية

إعداد الوارد

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

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() لكل قاعدة تنشئ protobuf router.RoutingRule.

بنية core.Config Protobuf

الملف: core/config.go

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

قائمة App هي نقطة التوسع الأساسية. أي ميزة (DNS، التوجيه، الإحصائيات، المرصد، الوكيل العكسي، المتحكم) تُضاف هنا كـ 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.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() بالتهيئة المؤجلة: يمكن لمصنع ميزة أن يطلب ميزات أخرى لم تُنشأ بعد. يحل المحرك الأساسي هذه التبعيات بعد تسجيل جميع الميزات، مما يُفعّل الاستدعاءات المؤجلة.

تحليل تقني لأغراض إعادة التنفيذ.