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البنية المعمارية
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:
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
}انتقالات الحالة
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 الفعلية
- الحشو: بايتات عشوائية لإخفاء أحجام الحزم
حساب حجم الحشو
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 الداخلي:
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
يُضيف الكاتب حشوًا أثناء المصافحة ويتحول إلى النسخ المباشر بعدها:
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
يُزيل القارئ الحشو ويتحول إلى القراءة المباشرة:
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:
// 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) للنقل بدون نسخ:
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 الخام تحت جميع الأغلفة:
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
}ملاحظات التنفيذ
Vision خاص بـ Go: الوصول عبر
unsafe.Pointerإلى المخازن الداخلية لـ TLS يعمل فقط مع تنفيذ TLS الخاص بـ Go. اللغات الأخرى تحتاج نهجًا بديلاً:- تنفيذ TLS مخصص مع حالة مصافحة مكشوفة
- TLS مبني على BIO (OpenSSL) حيث تتحكم في مخزن القراءة
- ربط طبقة سجل TLS
الحشو ضروري: بدون الحشو، تكشف أحجام سجلات مصافحة TLS عن SNI وبيانات بصمة أخرى. الحشو متغير الطول يجعل السجلات غير قابلة للتمييز.
TLS 1.3 فقط للنسخ المباشر: يتحول Vision إلى النسخ المباشر فقط لـ TLS 1.3 (حيث بيانات التطبيق مشفرة). لـ TLS 1.2، ينتهي الحشو لكن التأطير يستمر.
كشف السجل الكامل: تتحقق
IsCompleteRecord()من أن المخزن يحتوي على سجلات بيانات تطبيق TLS كاملة (0x17 0x03 0x03 + الطول). السجلات الجزئية لا يجب أن تُفعّل التحول إلى النسخ المباشر.Splice يتطلب جاهزية الطرفين: يجب أن يكون كل من المدخل والمخرج
CanSpliceCopy == 1قبل استخدام splice. الانتقال من 2→1 يحدث عندما يُؤكد Vision اكتمال المصافحة.سكون 1 مللي ثانية قبل splice:
time.Sleep(time.Millisecond)قبل splice هو حل بديل لحالة سباق نادرة حيث لم ينتهِ مكدس TLS من معالجة آخر سجل بالكامل.UserUUID في الإطار الأول: يتضمن أول إطار حشو معرف UserUUID بطول 16 بايت للمصادقة. الإطارات اللاحقة تحذفه (يُعيّن إلى nil). يتحقق الخادم من مطابقته للمستخدم المُصادق عليه.