XTLS Vision
XTLS Vision 消除了内层协议为 TLS 时的双重加密。它不再将已加密的 TLS Application Data 通过外层 TLS 重新加密,而是检测内层 TLS 握手完成的时刻并切换为直接透传——外层 TLS 仅包装内层 TLS 记录而不再重新加密。
源码:proxy/proxy.go、proxy/vless/encoding/encoding.go
双重加密问题
不使用 Vision 时:
[App Data] → [内层 TLS 加密] → [VLESS 帧] → [外层 TLS 加密] → 网络
↑ 加密一次 ↑ 加密两次!使用 Vision 后:
握手阶段: [TLS Handshake] → [Vision Padding] → [外层 TLS] → 网络
数据阶段: [TLS App Data] → [直接拷贝] → [外层 TLS] → 网络
↑ 无 VLESS 帧封装,无双重加密架构
flowchart TB
subgraph Client["客户端"]
AppTLS["应用 ←→ 内层 TLS"]
VW["VisionWriter<br/>(添加填充)"]
VR["VisionReader<br/>(移除填充)"]
OuterTLS["外层 TLS 连接"]
end
subgraph Server["服务端"]
SVR["VisionReader<br/>(移除填充)"]
SVW["VisionWriter<br/>(添加填充)"]
SOuterTLS["外层 TLS 连接"]
Freedom["Freedom 出站"]
end
AppTLS -->|"TLS 记录"| VW
VW -->|"填充后的帧"| OuterTLS
OuterTLS -->|"加密数据"| SOuterTLS
SOuterTLS -->|"填充后的帧"| SVR
SVR -->|"TLS 记录"| Freedom
Freedom -->|"响应"| SVW
SVW -->|"填充后的帧"| SOuterTLS
SOuterTLS --> OuterTLS
OuterTLS --> VR
VR -->|"响应"| AppTLS流量状态机
Vision 通过 TrafficState 追踪内层 TLS 连接的状态:
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: 连接开始
Padding --> Padding: TLS 握手数据包<br/>(添加/移除填充)
Padding --> FilterTLS: 分析数据包
FilterTLS --> FilterTLS: 查找 ServerHello
FilterTLS --> DetectedTLS13: 检测到 TLS 1.3
FilterTLS --> DetectedTLS12: 检测到 TLS 1.2
FilterTLS --> NotTLS: 非 TLS 流量
DetectedTLS13 --> WaitAppData: EnableXtls = true
WaitAppData --> DirectCopy: 首个 Application Data 记录
DetectedTLS12 --> PaddingEnd: 结束填充
NotTLS --> PaddingEnd: 结束填充
PaddingEnd --> NormalCopy: 不再填充
DirectCopy --> [*]: 直接 splice/拷贝<br/>(绕过所有帧封装)Vision 填充格式
在握手阶段,Vision 为每个数据块添加填充:
+-----------+-----+-------+-------+---------+---------+
| UserUUID | Cmd | Content Len | Padding Len |
| (16B,opt) | (1B)| (2B BE) | (2B BE) |
+-----------+-----+-------+-------+---------+---------+
| Content (contentLen bytes) | Padding (random) |
+---------------------------------+-------------------+- UserUUID:仅发送一次(首帧),之后设为 nil
- Command:
0x00=继续,0x01=结束,0x02=直接透传(切换到直通模式) - Content:实际的 TLS 记录数据
- Padding:随机字节,用于混淆数据包大小
填充大小计算
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] —— 即表示 TLS 1.3 的 supported_versions 扩展。
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 内部状态(客户端)
关键且有争议的部分——访问 Go TLS 的 input 和 rawInput 缓冲区:
// 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 内部缓冲区仅适用于 Go 的 TLS 实现。其他语言需要替代方案:- 暴露握手状态的自定义 TLS 实现
- 基于 BIO 的 TLS(OpenSSL),可控制读取缓冲区
- 钩入 TLS 记录层
填充至关重要:如果没有填充,TLS 握手记录的大小会暴露 SNI 及其他指纹信息。可变长度的填充使记录无法区分。
仅 TLS 1.3 支持直接拷贝:Vision 仅对 TLS 1.3 切换到直接拷贝(此时 Application Data 已加密)。对于 TLS 1.2,填充结束但帧封装继续。
完整记录检测:
IsCompleteRecord()检查缓冲区是否包含完整的 TLS Application Data 记录(0x17 0x03 0x03 + 长度)。不完整的记录不应触发直接拷贝切换。Splice 需要双端就绪:入站和出站都必须
CanSpliceCopy == 1才能使用 splice。从 2 到 1 的转换发生在 Vision 确认握手完成时。Splice 前的 1ms 睡眠:splice 前的
time.Sleep(time.Millisecond)是针对罕见竞态条件的变通方案,此时 TLS 协议栈可能尚未完全处理最后一条记录。首帧中的 UserUUID:首个填充帧包含 16 字节的 UserUUID 用于认证。后续帧省略(设为 nil)。服务器验证其是否与已认证的用户匹配。