Skip to content

XTLS Vision

يُزيل XTLS Vision التشفير المزدوج عندما يكون البروتوكول الداخلي هو TLS. بدلاً من تشفير بيانات تطبيق TLS المشفرة بالفعل عبر طبقة TLS الخارجية، يكشف Vision عن اكتمال مصافحة TLS الداخلية ويتحول إلى التمرير المباشر — حيث تُغلّف TLS الخارجية سجلات TLS الداخلية فقط دون إعادة تشفيرها.

المصدر: proxy/proxy.go، proxy/vless/encoding/encoding.go

مشكلة التشفير المزدوج

بدون Vision:

[App Data] → [Inner TLS Encrypt] → [VLESS Frame] → [Outer TLS Encrypt] → Network
                    ↑ encrypted once                      ↑ encrypted twice!

مع Vision:

Handshake phase:  [TLS Handshake] → [Vision Padding] → [Outer TLS] → Network
Data phase:       [TLS App Data] → [Direct Copy] → [Outer TLS] → Network
                                     ↑ no VLESS framing, no double encryption

البنية المعمارية

mermaid
flowchart TB
    subgraph Client["Client Side"]
        AppTLS["App ←→ Inner TLS"]
        VW["VisionWriter<br/>(adds padding)"]
        VR["VisionReader<br/>(removes padding)"]
        OuterTLS["Outer TLS Connection"]
    end

    subgraph Server["Server Side"]
        SVR["VisionReader<br/>(removes padding)"]
        SVW["VisionWriter<br/>(adds padding)"]
        SOuterTLS["Outer TLS Connection"]
        Freedom["Freedom Outbound"]
    end

    AppTLS -->|"TLS records"| VW
    VW -->|"padded frames"| OuterTLS
    OuterTLS -->|"encrypted"| SOuterTLS
    SOuterTLS -->|"padded frames"| SVR
    SVR -->|"TLS records"| Freedom

    Freedom -->|"response"| SVW
    SVW -->|"padded frames"| SOuterTLS
    SOuterTLS --> OuterTLS
    OuterTLS --> VR
    VR -->|"response"| AppTLS

آلة حالة حركة المرور

يتتبع Vision حالة اتصال TLS الداخلي عبر TrafficState:

go
type TrafficState struct {
    UserUUID               []byte
    NumberOfPacketToFilter int      // packets remaining to analyze
    EnableXtls             bool     // inner TLS 1.3 detected
    IsTLS12orAbove         bool
    IsTLS                  bool
    Cipher                 uint16   // detected cipher suite
    RemainingServerHello   int32
    Inbound                InboundState
    Outbound               OutboundState
}

انتقالات الحالة

mermaid
stateDiagram-v2
    [*] --> Padding: Connection starts
    Padding --> Padding: TLS Handshake packets<br/>(add/remove padding)
    Padding --> FilterTLS: Analyzing packets
    FilterTLS --> FilterTLS: Looking for ServerHello
    FilterTLS --> DetectedTLS13: Found TLS 1.3
    FilterTLS --> DetectedTLS12: Found TLS 1.2
    FilterTLS --> NotTLS: Not TLS traffic

    DetectedTLS13 --> WaitAppData: EnableXtls = true
    WaitAppData --> DirectCopy: First Application Data record

    DetectedTLS12 --> PaddingEnd: End padding
    NotTLS --> PaddingEnd: End padding

    PaddingEnd --> NormalCopy: No more padding

    DirectCopy --> [*]: Direct splice/copy<br/>(bypass all framing)

تنسيق حشو Vision

أثناء مرحلة المصافحة، يُغلّف Vision كل قطعة بيانات بحشو:

+-----------+-----+-------+-------+---------+---------+
| UserUUID  | Cmd | Content Len   | Padding Len       |
| (16B,opt) | (1B)| (2B BE)       | (2B BE)           |
+-----------+-----+-------+-------+---------+---------+
| Content (contentLen bytes)      | Padding (random)  |
+---------------------------------+-------------------+
  • UserUUID: يُرسل مرة واحدة فقط (الإطار الأول)، ثم يُعيّن إلى nil
  • الأمر: 0x00=متابعة، 0x01=إنهاء، 0x02=مباشر (التحول إلى التمرير)
  • المحتوى: بيانات سجل TLS الفعلية
  • الحشو: بايتات عشوائية لإخفاء أحجام الحزم

حساب حجم الحشو

go
func XtlsPadding(b *buf.Buffer, command byte, userUUID *[]byte,
    longPadding bool, ctx context.Context, testseed []uint32) *buf.Buffer {

    contentLen := b.Len()

    if contentLen < testseed[0] && longPadding {
        // Long padding: rand(testseed[1]) + testseed[2] - contentLen
        paddingLen = rand(testseed[1]) + testseed[2] - contentLen
    } else {
        // Short padding: rand(testseed[3])
        paddingLen = rand(testseed[3])
    }

    // Cap to buffer size
    if paddingLen > buf.Size - 21 - contentLen {
        paddingLen = buf.Size - 21 - contentLen
    }
}

قيم testseed الافتراضية: [900, 500, 900, 256] — وهذا يعني:

  • للمحتوى أقل من 900 بايت (المصافحة): حشو إلى ~900-1400 بايت
  • للمحتوى 900 بايت فأكثر: حشو 0-256 بايت

تحليل TLS (XtlsFilterTls)

يفحص Vision أول ~8 حزم للكشف عن إصدار TLS الداخلي:

go
func XtlsFilterTls(buffer, trafficState, ctx) {
    for each packet {
        // Look for TLS ServerHello (0x16 0x03 0x03 ... 0x02)
        if bytes.Equal(TlsServerHandShakeStart, packet[:3]) &&
           packet[5] == TlsHandshakeTypeServerHello {
            // Found ServerHello
            trafficState.RemainingServerHello = recordLength + 5
            trafficState.IsTLS12orAbove = true

            // Extract cipher suite
            sessionIdLen := packet[43]
            cipherSuite = packet[43+sessionIdLen+1 : 43+sessionIdLen+3]
            trafficState.Cipher = uint16(cipherSuite)
        }

        // Search ServerHello for TLS 1.3 indicator
        if trafficState.RemainingServerHello > 0 {
            if bytes.Contains(packet, Tls13SupportedVersions) {
                // TLS 1.3 confirmed!
                trafficState.EnableXtls = true
            }
        }
    }
}

نمط Tls13SupportedVersions هو [0x00, 0x2b, 0x00, 0x02, 0x03, 0x04] — امتداد supported_versions الذي يشير إلى TLS 1.3.

VisionWriter

يُضيف الكاتب حشوًا أثناء المصافحة ويتحول إلى النسخ المباشر بعدها:

go
func (w *VisionWriter) WriteMultiBuffer(mb) error {
    if *switchToDirectCopy {
        // Unwrap all encryption layers
        rawConn, _, writerCounter := UnwrapRawConn(w.conn)
        w.Writer = buf.NewWriter(rawConn)
        return w.Writer.WriteMultiBuffer(mb)
    }

    if *isPadding {
        // Check for TLS Application Data (0x17 0x03 0x03)
        for each buffer {
            if isTLS && isApplicationData && isCompleteRecord {
                if EnableXtls {
                    *switchToDirectCopy = true
                    command = CommandPaddingDirect  // tell peer to switch
                } else {
                    command = CommandPaddingEnd
                }
                buffer = XtlsPadding(buffer, command, ...)
                *isPadding = false
            } else {
                buffer = XtlsPadding(buffer, CommandPaddingContinue, ...)
            }
        }
    }
    return w.Writer.WriteMultiBuffer(mb)
}

VisionReader

يُزيل القارئ الحشو ويتحول إلى القراءة المباشرة:

go
func (w *VisionReader) ReadMultiBuffer() (buf.MultiBuffer, error) {
    buffer, err := w.Reader.ReadMultiBuffer()

    if *switchToDirectCopy {
        return buffer, err  // already in direct mode
    }

    if *withinPaddingBuffers {
        // Remove padding from each buffer
        for each buffer {
            newbuffer = XtlsUnpadding(buffer, trafficState, ...)
        }

        // Check command
        if command == 0 { continue padding }
        if command == 1 { stop padding, normal copy }
        if command == 2 { stop padding, switch to DIRECT COPY }
    }

    if *switchToDirectCopy {
        // Drain TLS internal buffers (input + rawInput)
        inputBuffer = buf.ReadFrom(w.input)
        rawInputBuffer = buf.ReadFrom(w.rawInput)
        buffer = merge(buffer, inputBuffer, rawInputBuffer)

        // Switch to raw connection reader
        readerConn, readCounter, _ := UnwrapRawConn(w.conn)
        w.Reader = buf.NewReader(readerConn)
    }
}

الوصول إلى الحالة الداخلية لـ TLS (العميل)

القطعة الحاسمة (والمثيرة للجدل) — الوصول إلى مخازن input وrawInput الداخلية لـ TLS في Go:

go
// In VLESS outbound:
if tlsConn, ok := iConn.(*tls.Conn); ok {
    t = reflect.TypeOf(tlsConn.Conn).Elem()
    p = uintptr(unsafe.Pointer(tlsConn.Conn))
} else if utlsConn, ok := iConn.(*tls.UConn); ok {
    t = reflect.TypeOf(utlsConn.Conn).Elem()
    p = uintptr(unsafe.Pointer(utlsConn.Conn))
} else if realityConn, ok := iConn.(*reality.UConn); ok {
    t = reflect.TypeOf(realityConn.Conn).Elem()
    p = uintptr(unsafe.Pointer(realityConn.Conn))
}

i, _ := t.FieldByName("input")     // *bytes.Reader
r, _ := t.FieldByName("rawInput")  // *bytes.Buffer
input = (*bytes.Reader)(unsafe.Pointer(p + i.Offset))
rawInput = (*bytes.Buffer)(unsafe.Pointer(p + r.Offset))

هذه المخازن الداخلية تحتوي على بيانات فكّت تشفيرها طبقة TLS الخارجية لكن التطبيق لم يقرأها بعد. عند التحول إلى النسخ المباشر، يجب تصريف هذه البيانات أولاً.

نسخ Splice (CopyRawConnIfExist)

على Linux، يمكن لـ Vision استخدام splice(2) للنقل بدون نسخ:

go
func CopyRawConnIfExist(ctx, readerConn, writerConn, writer, timer, inTimer) error {
    readerConn, readCounter, _ = UnwrapRawConn(readerConn)
    writerConn, _, writeCounter = UnwrapRawConn(writerConn)

    // Check splice eligibility
    if runtime.GOOS != "linux" { return readV(...) }
    tc, ok := writerConn.(*net.TCPConn)
    if !ok { return readV(...) }
    if inbound.CanSpliceCopy == 3 { return readV(...) }

    // Wait for both sides to be ready
    for {
        if inbound.CanSpliceCopy == 1 && ob.CanSpliceCopy == 1 {
            // SPLICE! Zero-copy kernel transfer
            w, err := tc.ReadFrom(readerConn)
            return err
        }
        // Not ready yet, do normal copy
        buffer, err := reader.ReadMultiBuffer()
        writer.WriteMultiBuffer(buffer)
    }
}

قيم CanSpliceCopy

القيمةالمعنى
1جاهز لـ splice
2سيكون جاهزًا بعد معالجة البروتوكول
3لا يمكن splice (TUN، نقل غير خام، إلخ)

UnwrapRawConn

لتنفيذ splice، نحتاج اتصال TCP الخام تحت جميع الأغلفة:

go
func UnwrapRawConn(conn) (net.Conn, readCounter, writeCounter) {
    // Peel layers: encryption → stats → TLS/uTLS/REALITY → proxyproto → unix
    if commonConn, ok := conn.(*encryption.CommonConn); ok { conn = commonConn.Conn }
    if xorConn, ok := conn.(*encryption.XorConn); ok { return xorConn }  // full-random: don't penetrate
    if statConn, ok := conn.(*stat.CounterConnection); ok { conn = statConn.Connection }
    if tlsConn, ok := conn.(*tls.Conn); ok { conn = tlsConn.NetConn() }
    // ... utls, reality, proxyproto, unix
    return conn
}

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

  1. Vision خاص بـ Go: الوصول عبر unsafe.Pointer إلى المخازن الداخلية لـ TLS يعمل فقط مع تنفيذ TLS الخاص بـ Go. اللغات الأخرى تحتاج نهجًا بديلاً:

    • تنفيذ TLS مخصص مع حالة مصافحة مكشوفة
    • TLS مبني على BIO (OpenSSL) حيث تتحكم في مخزن القراءة
    • ربط طبقة سجل TLS
  2. الحشو ضروري: بدون الحشو، تكشف أحجام سجلات مصافحة TLS عن SNI وبيانات بصمة أخرى. الحشو متغير الطول يجعل السجلات غير قابلة للتمييز.

  3. TLS 1.3 فقط للنسخ المباشر: يتحول Vision إلى النسخ المباشر فقط لـ TLS 1.3 (حيث بيانات التطبيق مشفرة). لـ TLS 1.2، ينتهي الحشو لكن التأطير يستمر.

  4. كشف السجل الكامل: تتحقق IsCompleteRecord() من أن المخزن يحتوي على سجلات بيانات تطبيق TLS كاملة (0x17 0x03 0x03 + الطول). السجلات الجزئية لا يجب أن تُفعّل التحول إلى النسخ المباشر.

  5. Splice يتطلب جاهزية الطرفين: يجب أن يكون كل من المدخل والمخرج CanSpliceCopy == 1 قبل استخدام splice. الانتقال من 2→1 يحدث عندما يُؤكد Vision اكتمال المصافحة.

  6. سكون 1 مللي ثانية قبل splice: time.Sleep(time.Millisecond) قبل splice هو حل بديل لحالة سباق نادرة حيث لم ينتهِ مكدس TLS من معالجة آخر سجل بالكامل.

  7. UserUUID في الإطار الأول: يتضمن أول إطار حشو معرف UserUUID بطول 16 بايت للمصادقة. الإطارات اللاحقة تحذفه (يُعيّن إلى nil). يتحقق الخادم من مطابقته للمستخدم المُصادق عليه.

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