Skip to content

بروتوكول VMess

VMess هو بروتوكول الوكيل المشفر الأصلي لـ V2Ray/Xray. يوفر تشفيراً مُصادقاً مع خيارات تشفير متعددة، وحماية ضد إعادة التشغيل مبنية على الوقت، وحشو/تقنيع اختياري لمقاومة تحليل حركة المرور.

نظرة عامة

  • الاتجاه: وارد + صادر
  • النقل: TCP، مقبس UNIX
  • التشفير: AES-128-GCM، ChaCha20-Poly1305، بدون
  • المصادقة: مبنية على UUID مع تشفير ترويسة AEAD
  • Mux: مدعوم عبر النطاق الافتراضي v1.mux.cool وXUDP

صيغة السلك (Wire Format)

الطلب (من العميل إلى الخادم)

يتكون طلب VMess AEAD من طبقتين: غلاف خارجي مشفر بـ AEAD، وترويسة الأمر الداخلية.

الغلاف الخارجي (ترويسة AEAD)

+----------+---------------------------+--------------+---------------------------+
| Auth ID  | Encrypted Payload Length  | Conn Nonce   | Encrypted Payload         |
| 16 bytes | 2 + 16 bytes (GCM tag)    | 8 bytes      | variable + 16 bytes (tag) |
+----------+---------------------------+--------------+---------------------------+

المصدر: proxy/vmess/aead/encrypt.go:14-61

معرّف المصادقة (Auth ID) (16 بايت): كتلة مشفرة بـ AES-ECB تحتوي على:

+---------------+-----------+----------+
| Timestamp     | Random    | CRC32    |
| 8 bytes (BE)  | 4 bytes   | 4 bytes  |
+---------------+-----------+----------+

يُشتق مفتاح AES لتشفير Auth ID عبر:

go
aesKey = KDF16(cmdKey, "AES Auth ID Encryption")

المصدر: proxy/vmess/aead/authid.go:26-40

التحقق من الوقت: يفك الخادم تشفير Auth ID لكل مستخدم معروف، ويتحقق من CRC32، ويتأكد من أن الطابع الزمني ضمن 120 ثانية من وقت الخادم.

المصدر: proxy/vmess/aead/authid.go:99-121

تشفير طول الحمولة: AES-128-GCM بمفتاح/nonce مشتق من:

go
key   = KDF16(cmdKey, "VMess Header AEAD Key_Length", authID, connNonce)
nonce = KDF(cmdKey, "VMess Header AEAD Nonce_Length", authID, connNonce)[:12]
// Additional data = authID

تشفير الحمولة: AES-128-GCM بمفتاح/nonce مشتق من:

go
key   = KDF16(cmdKey, "VMess Header AEAD Key", authID, connNonce)
nonce = KDF(cmdKey, "VMess Header AEAD Nonce", authID, connNonce)[:12]
// Additional data = authID

المصدر: proxy/vmess/aead/encrypt.go:30-51

ترويسة الأمر الداخلية (الحمولة المفكوكة التشفير)

+-----+--------+--------+---------+--------+----------+-----+---------+---------+-------+
| Ver | BodyIV | BodyKey | RespHdr | Option | Security | Rsv | Command | Address | Pad   | FNV1a |
| 1B  | 16B    | 16B    | 1B      | 1B     | 1B       | 1B  | 1B      | var     | 0-15B | 4B    |
+-----+--------+--------+---------+--------+----------+-----+---------+---------+-------+

المصدر: proxy/vmess/encoding/client.go:63-101

الحقلالحجمالوصف
Version1 بايتدائماً 0x01
Body IV16 بايتIV عشوائي لتشفير المحتوى
Body Key16 بايتمفتاح عشوائي لتشفير المحتوى
Response Header1 بايتبايت عشوائي يجب أن يُعيده الخادم
Option1 بايتقناع بتات: ChunkStream(0x01)، ChunkMasking(0x04)، GlobalPadding(0x08)، AuthenticatedLength(0x10)
Security1 بايتالبتات العليا 4 = طول الحشو (0-15)، البتات السفلى 4 = نوع التشفير
Reserved1 بايتدائماً 0x00
Command1 بايت0x01=TCP، 0x02=UDP، 0x03=Mux
Addressمتغيرالمنفذ(2 بايت, BE) + نوع العنوان(1 بايت) + العنوان
Padding0-15 بايتحشو عشوائي
FNV1a4 بايتتجزئة FNV-1a لجميع الحقول السابقة (للتكامل)

أنواع التشفير (البتات السفلى 4):

القيمةالتشفير
0x00AUTO / UNKNOWN
0x03AES-128-GCM
0x04ChaCha20-Poly1305
0x05بدون

المصدر: proxy/vmess/encoding/server.go:114-124

الاستجابة (من الخادم إلى العميل)

ترويسة الاستجابة مشفرة أيضاً بـ AEAD:

+----------------------------+----------------------------+
| Encrypted Length (2+16B)   | Encrypted Header (var+16B) |
+----------------------------+----------------------------+

المفاتيح مشتقة من مفتاح/IV محتوى الاستجابة:

go
responseBodyKey = SHA256(requestBodyKey)[:16]
responseBodyIV  = SHA256(requestBodyIV)[:16]

lengthKey   = KDF16(responseBodyKey, "AEAD Resp Header Len Key")
lengthNonce = KDF(responseBodyIV, "AEAD Resp Header Len IV")[:12]
payloadKey  = KDF16(responseBodyKey, "AEAD Resp Header Key")
payloadNonce = KDF(responseBodyIV, "AEAD Resp Header IV")[:12]

المصدر: proxy/vmess/encoding/client.go:179-253، proxy/vmess/encoding/server.go:328-369

ترويسة الاستجابة بعد فك التشفير:

+----------+--------+---------+---------+
| RespHdr  | Option | CmdID   | CmdLen  | CmdData |
| 1B       | 1B     | 1B      | 1B      | var     |
+----------+--------+---------+---------+

يجب أن يتطابق بايت RespHdr مع البايت العشوائي من الطلب.

تشفير المحتوى

يُنقل محتوى البيانات كأجزاء مُصادقة:

mermaid
graph LR
    subgraph "Each Chunk"
        A[Length 2B] --> B[Payload] --> C[Auth Tag]
    end

ترميز حجم الجزء (مع التقنيع): عند تفعيل ChunkMasking، يقوم تدفق SHAKE128 بعملية XOR على حقل الطول ذي البايتين:

go
// ShakeSizeParser
mask = SHAKE128(bodyIV).next_2_bytes()
wire_length = actual_length XOR mask

المصدر: proxy/vmess/encoding/auth.go:51-87

توليد nonce لـ AEAD المحتوى:

go
func GenerateChunkNonce(nonce []byte, size uint32) BytesGenerator {
    c := copy(nonce)
    count := uint16(0)
    return func() []byte {
        binary.BigEndian.PutUint16(c, count)
        count++
        return c[:size]
    }
}

يكون nonce هو IV المحتوى مع استبدال أول بايتين بعداد متزايد.

المصدر: proxy/vmess/encoding/client.go:332-340

اشتقاق مفتاح ChaCha20-Poly1305: يُوسّع مفتاح المحتوى ذو 16 بايت إلى 32 بايت:

go
func GenerateChacha20Poly1305Key(b []byte) []byte {
    key := make([]byte, 32)
    t := md5.Sum(b)
    copy(key, t[:])
    t = md5.Sum(key[:16])
    copy(key[16:], t[:])
    return key
}

المصدر: proxy/vmess/encoding/auth.go:42-49

إشارة الإنهاء: عندما لا يكون NoTerminationSignal مُعيّناً، يشير جزء فارغ (طول=0) إلى نهاية التدفق.

المصدر: proxy/vmess/outbound/outbound.go:185-189

دالة اشتقاق المفاتيح (KDF)

يستخدم VMess دالة KDF مبنية على HMAC-SHA256 متداخلة:

go
func KDF(key []byte, path ...string) []byte {
    hmacf := hmac.New(sha256.New, []byte("VMess AEAD KDF"))
    for _, v := range path {
        hmacf = hmac.New(func() hash.Hash {
            // uses previous hmac as inner hash
            return hmacf
        }, []byte(v))
    }
    hmacf.Write(key)
    return hmacf.Sum(nil)
}

المصدر: proxy/vmess/aead/kdf.go:13-28

ثوابت أملاح KDF:

الثابتالقيمة
KDFSaltConstVMessAEADKDF"VMess AEAD KDF"
KDFSaltConstAuthIDEncryptionKey"AES Auth ID Encryption"
KDFSaltConstVMessHeaderPayloadAEADKey"VMess Header AEAD Key"
KDFSaltConstVMessHeaderPayloadAEADIV"VMess Header AEAD Nonce"
KDFSaltConstVMessHeaderPayloadLengthAEADKey"VMess Header AEAD Key_Length"
KDFSaltConstVMessHeaderPayloadLengthAEADIV"VMess Header AEAD Nonce_Length"
KDFSaltConstAEADRespHeaderLenKey"AEAD Resp Header Len Key"
KDFSaltConstAEADRespHeaderLenIV"AEAD Resp Header Len IV"
KDFSaltConstAEADRespHeaderPayloadKey"AEAD Resp Header Key"
KDFSaltConstAEADRespHeaderPayloadIV"AEAD Resp Header IV"

المصدر: proxy/vmess/aead/consts.go:1-14

مصادقة المستخدم

مفتاح الأمر (CmdKey)

يُشتق CmdKey من UUID المستخدم:

go
func NewID(uuid uuid.UUID) *ID {
    // cmdKey = MD5(uuid.Bytes() + []byte("c48619fe-8f02-49e0-b9e9-edf763e17e21"))
}

المصدر: common/protocol/id.go

مُدقق المستخدم المؤقت (TimedUserValidator)

يحتفظ الخادم بـ TimedUserValidator الذي:

  1. يخزّن جميع مفاتيح CmdKey للمستخدمين في AuthIDDecoderHolder
  2. لكل اتصال وارد، يحاول فك تشفير AES لـ Auth ID ذي 16 بايت بمفتاح كل مستخدم
  3. يتحقق من تجزئة CRC32، ونطاق الطابع الزمني (+-120 ثانية)، ومرشح إعادة التشغيل
go
func (a *AuthIDDecoderHolder) Match(authID [16]byte) (interface{}, error) {
    for _, v := range a.decoders {
        t, z, _, d := v.dec.Decode(authID)
        if z != crc32.ChecksumIEEE(d[:12]) { continue }
        if math.Abs(float64(t) - float64(time.Now().Unix())) > 120 { return nil, ErrInvalidTime }
        if !a.filter.Check(authID) { return nil, ErrReplay }
        return v.ticket, nil
    }
    return nil, ErrNotFound
}

المصدر: proxy/vmess/aead/authid.go:99-121

بذرة السلوك (Behavior Seed)

تُحسب "بذرة سلوك" حتمية من جميع معرّفات المستخدمين باستخدام HMAC-SHA256 + CRC64. تتحكم هذه البذرة في نمط التصريف (أطوال قراءة عشوائية قبل إغلاق الاتصالات غير الصالحة) لمنع هجمات الاستقصاء.

المصدر: proxy/vmess/validator.go:114-123

المعالج الوارد

الملف: proxy/vmess/inbound/inbound.go

يقوم المعالج الوارد بما يلي:

  1. تعيين مهلة قراءة المصافحة من السياسة
  2. إنشاء ServerSession مع TimedUserValidator
  3. استدعاء DecodeRequestHeader() للمصادقة والتحليل
  4. التوزيع إلى موزّع التوجيه
  5. تشغيل الطلب (القراءة من العميل، الكتابة إلى الرابط) والاستجابة (القراءة من الرابط، الكتابة إلى العميل) كمهام متوازية

الأسطر الرئيسية: proxy/vmess/inbound/inbound.go:226-319

المعالج الصادر

الملف: proxy/vmess/outbound/outbound.go

يقوم المعالج الصادر بما يلي:

  1. اختيار الخادم والمستخدم من الإعدادات
  2. تحديد نوع التشفير من إعدادات الحساب
  3. تفعيل ChunkMasking وGlobalPadding تلقائياً لتشفيرات AEAD
  4. دعم SecurityType_ZERO (بدون تشفير، بدون تجزئة) لحالات استخدام XTLS
  5. إنشاء ClientSession بمفتاح/IV عشوائي للمحتوى
  6. ترميز ترويسة الطلب + المحتوى، ثم قراءة الاستجابة

الأسطر الرئيسية: proxy/vmess/outbound/outbound.go:57-225

ملاحظات التنفيذ

  1. إزالة البروتوكول القديم: تمت إزالة مصادقة VMess القديمة غير AEAD (المبنية على MD5) بالكامل. يُدعم AEAD فقط. إذا لم يتمكن الخادم من مطابقة أي مستخدم عبر فك تشفير AEAD، يُرجع خطأ مع تصريف حتمي.

  2. حماية إعادة تشغيل الجلسة: طبقتان — AuthIDDecoderHolder.filter (مرشح خريطة 120 ثانية على Auth IDs) وSessionHistory (ذاكرة مؤقتة لمدة 3 دقائق من مجموعات {user, bodyKey, bodyIV}).

  3. الحشو: عند تفعيل GlobalPadding، تُرجع ShakeSizeParser.NextPaddingLen() القيمة shake128(IV) % 64، مما يضيف 0-63 بايت من الحشو العشوائي لكل جزء.

  4. تجربة الطول المُصادق: عند التفعيل عبر TestsEnabled: "AuthenticatedLength"، تُشفّر أحجام الأجزاء نفسها بـ AEAD باستخدام مفتاح منفصل مشتق من KDF16(bodyKey, "auth_len").

  5. UDP عبر Mux: يُغلّف VMess حركة UDP كاتصالات Mux إلى v1.mux.cool:666، مستخدماً تأطير XUDP.

  6. صيغة العنوان: يستخدم VMess ترتيب المنفذ-ثم-العنوان (بخلاف SOCKS5 الذي يستخدم العنوان-ثم-المنفذ). بايتات نوع العنوان: 0x01=IPv4، 0x02=نطاق، 0x03=IPv6.

المصدر: proxy/vmess/encoding/encoding.go:12-17

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