Skip to content

XTLS Vision

XTLS Vision 消除了内层协议为 TLS 时的双重加密。它不再将已加密的 TLS Application Data 通过外层 TLS 重新加密,而是检测内层 TLS 握手完成的时刻并切换为直接透传——外层 TLS 仅包装内层 TLS 记录而不再重新加密。

源码proxy/proxy.goproxy/vless/encoding/encoding.go

双重加密问题

不使用 Vision 时:

[App Data] → [内层 TLS 加密] → [VLESS 帧] → [外层 TLS 加密] → 网络
                    ↑ 加密一次                      ↑ 加密两次!

使用 Vision 后:

握手阶段:  [TLS Handshake] → [Vision Padding] → [外层 TLS] → 网络
数据阶段:  [TLS App Data] → [直接拷贝] → [外层 TLS] → 网络
                                 ↑ 无 VLESS 帧封装,无双重加密

架构

mermaid
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 连接的状态:

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: 连接开始
    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
  • Command0x00=继续, 0x01=结束, 0x02=直接透传(切换到直通模式)
  • Content:实际的 TLS 记录数据
  • Padding:随机字节,用于混淆数据包大小

填充大小计算

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] —— 即表示 TLS 1.3 的 supported_versions 扩展。

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 内部状态(客户端)

关键且有争议的部分——访问 Go TLS 的 inputrawInput 缓冲区:

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 内部缓冲区仅适用于 Go 的 TLS 实现。其他语言需要替代方案:

    • 暴露握手状态的自定义 TLS 实现
    • 基于 BIO 的 TLS(OpenSSL),可控制读取缓冲区
    • 钩入 TLS 记录层
  2. 填充至关重要:如果没有填充,TLS 握手记录的大小会暴露 SNI 及其他指纹信息。可变长度的填充使记录无法区分。

  3. 仅 TLS 1.3 支持直接拷贝:Vision 仅对 TLS 1.3 切换到直接拷贝(此时 Application Data 已加密)。对于 TLS 1.2,填充结束但帧封装继续。

  4. 完整记录检测IsCompleteRecord() 检查缓冲区是否包含完整的 TLS Application Data 记录(0x17 0x03 0x03 + 长度)。不完整的记录不应触发直接拷贝切换。

  5. Splice 需要双端就绪:入站和出站都必须 CanSpliceCopy == 1 才能使用 splice。从 2 到 1 的转换发生在 Vision 确认握手完成时。

  6. Splice 前的 1ms 睡眠:splice 前的 time.Sleep(time.Millisecond) 是针对罕见竞态条件的变通方案,此时 TLS 协议栈可能尚未完全处理最后一条记录。

  7. 首帧中的 UserUUID:首个填充帧包含 16 字节的 UserUUID 用于认证。后续帧省略(设为 nil)。服务器验证其是否与已认证的用户匹配。

用于重新实现目的的技术分析。