Skip to content

TCP 传输

简介

TCP 传输是 Xray-core 中默认且最基础的传输协议。它提供原始 TCP 连接,支持可选的 TLS/REALITY 安全层、HTTP 头部伪装以及高级套接字级别调优。本文档涵盖拨号/监听流程、套接字选项支持、Happy Eyeballs 双栈竞速以及透明代理(TProxy)支持。

协议注册

TCP 传输以 "tcp" 名称注册(transport/internet/tcp/tcp.go:3):

go
const protocolName = "tcp"

注册发生在三个 init() 函数中:

  • 拨号器tcp/dialer.go:110-112 -- RegisterTransportDialer("tcp", Dial)
  • 监听器tcp/hub.go:138-140 -- RegisterTransportListener("tcp", ListenTCP)
  • 配置创建器tcp/config.go:8-12 -- RegisterProtocolConfigCreator("tcp", ...)

拨号流程

入口点

tcp.Dialtcp/dialer.go:20-108)由传输分发层调用:

go
func Dial(ctx context.Context, dest net.Destination,
    streamSettings *internet.MemoryStreamConfig) (stat.Connection, error) {
    conn, err := internet.DialSystem(ctx, dest, streamSettings.SocketSettings)
    if err != nil {
        return nil, err
    }
    // 应用 TLS 或 REALITY...
    // 应用头部认证器...
    return stat.Connection(conn), nil
}

安全层应用

原始 TCP 连接建立后,按优先级顺序应用安全层(tcp/dialer.go:27-93):

  1. TLS:如果 tls.ConfigFromStreamSettings 返回非 nil,则执行 TLS 握手。若配置了指纹,使用 uTLS 指纹(tls.UClient),否则使用标准 Go TLS(tls.Client)。
  2. REALITY:如果 reality.ConfigFromStreamSettings 返回非 nil(且未配置 TLS),则通过 reality.UClient 执行 REALITY 握手。

头部伪装

安全层之后,可选的 ConnectionAuthenticator 包装连接(tcp/dialer.go:95-106):

go
tcpSettings := streamSettings.ProtocolSettings.(*Config)
if tcpSettings.HeaderSettings != nil {
    headerConfig, _ := tcpSettings.HeaderSettings.GetInstance()
    auth, _ := internet.CreateConnectionAuthenticator(headerConfig)
    conn = auth.Client(conn)
}

这实现了 HTTP 头部伪装,将 TCP 流包装成看起来像正常 HTTP 流量。

监听流程

入口点

tcp.ListenTCPtcp/hub.go:30-95)创建 TCP 监听器:

go
func ListenTCP(ctx context.Context, address net.Address, port net.Port,
    streamSettings *internet.MemoryStreamConfig, handler internet.ConnHandler) (internet.Listener, error) {
    // ...
    listener, err = internet.ListenSystem(ctx, &net.TCPAddr{
        IP:   address.IP(),
        Port: int(port),
    }, streamSettings.SocketSettings)
    // 配置 TLS / REALITY / 头部认证
    go l.keepAccepting()
    return l, nil
}

连接接受循环

keepAccepting 协程(tcp/hub.go:97-126)接受连接并应用安全层:

go
func (v *Listener) keepAccepting() {
    for {
        conn, err := v.listener.Accept()
        // ...
        go func() {
            if v.tlsConfig != nil {
                conn = tls.Server(conn, v.tlsConfig)
            } else if v.realityConfig != nil {
                conn, err = reality.Server(conn, v.realityConfig)
            }
            if v.authConfig != nil {
                conn = v.authConfig.Server(conn)
            }
            v.addConn(stat.Connection(conn))
        }()
    }
}

Unix 域套接字支持

拨号和监听均支持 Unix 域套接字。监听器通过检查 port == 0 来切换到 Unix 模式(tcp/hub.go:44-55)。

PROXY 协议

TCP 监听器合并来自 TCP 配置和套接字设置的 PROXY 协议接受选项(tcp/hub.go:37-41):

go
streamSettings.SocketSettings.AcceptProxyProtocol =
    l.config.AcceptProxyProtocol || streamSettings.SocketSettings.AcceptProxyProtocol

启用后,internet.ListenSystem 使用 proxyproto.Listener 包装监听器(system_listener.go:170-173)。

系统拨号器:套接字选项

DefaultSystemDialer.Dialtransport/internet/system_dialer.go:51-146)是创建原始操作系统连接的地方。它配置:

  • Keep-Alive:类 Chrome 默认值(45 秒空闲,45 秒间隔),通过 Go 1.24+ 的 net.KeepAliveConfig 实现(system_dialer.go:91-117
  • Multipath TCP:设置 TcpMptcp 时启用 dialer.SetMultipathTCP(true)system_dialer.go:121-123
  • 套接字控制Control 回调通过 applyOutboundSocketOptions 应用平台特定选项(system_dialer.go:124-143

Linux 套接字选项(sockopt_linux.go

Linux 实现(transport/internet/sockopt_linux.go:43-138)支持:

选项系统调用配置字段
SO_MARKSOL_SOCKET, SO_MARKconfig.Mark
SO_BINDTODEVICEBindToDeviceconfig.Interface
TCP_FASTOPEN_CONNECTSOL_TCP, TCP_FASTOPEN_CONNECTconfig.Tfo(出站)
TCP_FASTOPENSOL_TCP, TCP_FASTOPENconfig.Tfo(入站)
TCP_CONGESTIONSOL_TCP, TCP_CONGESTIONconfig.TcpCongestion
TCP_WINDOW_CLAMPIPPROTO_TCP, TCP_WINDOW_CLAMPconfig.TcpWindowClamp
TCP_USER_TIMEOUTIPPROTO_TCP, TCP_USER_TIMEOUTconfig.TcpUserTimeout
TCP_MAXSEGIPPROTO_TCP, TCP_MAXSEGconfig.TcpMaxSeg
IP_TRANSPARENTSOL_IP, IP_TRANSPARENTconfig.Tproxy
IPV6_RECVORIGDSTADDR多种config.ReceiveOriginalDestAddress
自定义用户定义的 level/optconfig.CustomSockopt

macOS 套接字选项(sockopt_darwin.go

Darwin 实现(transport/internet/sockopt_darwin.go:103-192)有平台特定的差异:

  • TCP_FASTOPEN 使用平台常量:TCP_FASTOPEN_SERVER = 0x01TCP_FASTOPEN_CLIENT = 0x02sockopt_darwin.go:19-21
  • 接口绑定 使用 IP_BOUND_IF / IPV6_BOUND_IF 替代 SO_BINDTODEVICEsockopt_darwin.go:136-151
  • 透明代理 使用 /dev/pfDIOCNATLOOK ioctl 进行原始目的地查找(sockopt_darwin.go:40-101

TFO 值解析

TFO 配置值有特殊的解析方式(transport/internet/sockopt.go:21-30):

go
func (v *SocketConfig) ParseTFOValue() int {
    if v.Tfo == 0 { return -1 }  // 未设置
    tfo := int(v.Tfo)
    if tfo < 0 { tfo = 0 }       // 显式禁用
    return tfo
}

在 Linux 出站时,任何正值都变为 1 用于 TCP_FASTOPEN_CONNECT。在 Linux 入站时,该值直接传递给 TCP_FASTOPEN 作为队列长度。

自定义套接字选项

CustomSockopt 机制(sockopt_linux.go:93-129)允许任意的 setsockopt 调用:

go
for _, custom := range config.CustomSockopt {
    if custom.System != "" && custom.System != runtime.GOOS { continue }
    if !strings.HasPrefix(network, custom.Network) { continue }
    // 应用整数或字符串 setsockopt
}

支持按操作系统和网络类型过滤(例如 "tcp" 匹配 tcp4/tcp6)。

Happy Eyeballs(RFC 8305)

当解析到多个 IP 且启用 Happy Eyeballs 时,TcpRaceDialtransport/internet/happy_eyeballs.go:16-97)实现 RFC 8305:

IP 排序

sortIPshappy_eyeballs.go:100-156)交错排列 IPv4 和 IPv6 地址:

go
func sortIPs(ips []net.IP, prioritizeIPv6 bool, interleave uint32) []net.IP {
    // 分离为 ip4 和 ip6 切片
    // 交错:根据 interleave 计数交替 ip4/ip6
    // prioritizeIPv6 控制哪个地址族优先
}

使用默认 interleave=1 时,结果为:[v6, v4, v6, v4, ...](如果 IPv6 不优先则为 [v4, v6, ...])。

竞速拨号算法

mermaid
sequenceDiagram
    participant C as TcpRaceDial
    participant T as 定时器
    participant G1 as 协程 1
    participant G2 as 协程 2
    participant G3 as 协程 3

    C->>T: 启动(首次延迟 0ms)
    T->>G1: tcpTryDial(IP[0])
    T->>T: Reset(tryDelayMs)
    T->>G2: tcpTryDial(IP[1])
    G1-->>C: result{conn, nil}
    C->>C: 取消上下文(停止其他尝试)
    C->>C: 等待活跃协程结束
    C-->>C: 返回获胜的连接

来自 HappyEyeballs 配置的关键参数:

  • TryDelayMs:每次新尝试之间的延迟(根据 RFC 8305 默认为 250ms)
  • MaxConcurrentTry:最大并发连接尝试数
  • PrioritizeIpv6:IPv6 是否在交错列表中优先
  • Interleave:在切换地址族之前使用多少个同族地址

首个成功的连接获胜,其他连接被关闭。算法优雅地处理取消和失败(happy_eyeballs.go:35-96)。

激活条件

仅在满足所有条件时使用 Happy Eyeballs(dialer.go:263):

  • HappyEyeballs 配置不为 nil
  • TryDelayMs > 0 且 MaxConcurrentTry > 0
  • 至少解析到 2 个 IP
  • 未配置 DialerProxy
  • 目标为 TCP

透明代理(TProxy)

Linux

在 Linux 上,TProxy 通过在套接字上设置 IP_TRANSPARENTsockopt_linux.go:131-135)来工作,允许进程绑定到非本地地址。原始目的地恢复使用 SO_ORIGINAL_DSTtcp/sockopt_linux.go:18-52):

go
func GetOriginalDestination(conn stat.Connection) (net.Destination, error) {
    // 使用 getsockopt(SO_ORIGINAL_DST) 从重定向连接(通过 iptables REDIRECT/TPROXY)
    // 恢复原始目的地
}

macOS

在 macOS 上,原始目的地通过 PF(sockopt_darwin.go:40-101)使用 /dev/pf 上的 DIOCNATLOOK ioctl 恢复。

系统监听器

DefaultListener.Listentransport/internet/system_listener.go:78-175)创建操作系统级别的监听器,包含:

  • 套接字控制:通过 getControlFuncsystem_listener.go:24-42)应用 applyInboundSocketOptions
  • SO_REUSEPORT:始终在监听器上设置(system_listener.go:39
  • Unix 域套接字:支持抽象套接字(Linux @ 前缀)、文件权限和文件锁(system_listener.go:115-166
  • PROXY 协议:启用时使用 proxyproto.Listener 包装(system_listener.go:170-173
  • Multipath TCP:配置后启用 lc.SetMultipathTCP(true)system_listener.go:111-113

实现说明

  • 默认协议:未指定传输协议时使用 TCP(config.go:59-63)。
  • UDP 包装:对于 UDP 目标,DefaultSystemDialer 创建 PacketConnWrapper,在 net.PacketConn 上实现 net.Connsystem_dialer.go:54-89)。
  • Keep-Alive 默认值:拨号器模拟 Chrome 的 keep-alive 行为:45 秒空闲 + 45 秒间隔(system_dialer.go:91-96)。监听器默认禁用 keep-alive(system_listener.go:92)。
  • FakePacketConn:用于将 TCP 连接包装为 PacketConn,用于 QUIC-over-TCP 场景(system_dialer.go:258-283)。
  • 拨号超时:硬编码为 16 秒(system_dialer.go:113)。
  • REALITY 监听器:配置 REALITY 后,监听器会启动一个协程用于 DetectPostHandshakeRecordsLenstcp/hub.go:78)。

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