TCP 传输
简介
TCP 传输是 Xray-core 中默认且最基础的传输协议。它提供原始 TCP 连接,支持可选的 TLS/REALITY 安全层、HTTP 头部伪装以及高级套接字级别调优。本文档涵盖拨号/监听流程、套接字选项支持、Happy Eyeballs 双栈竞速以及透明代理(TProxy)支持。
协议注册
TCP 传输以 "tcp" 名称注册(transport/internet/tcp/tcp.go:3):
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.Dial(tcp/dialer.go:20-108)由传输分发层调用:
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):
- TLS:如果
tls.ConfigFromStreamSettings返回非 nil,则执行 TLS 握手。若配置了指纹,使用 uTLS 指纹(tls.UClient),否则使用标准 Go TLS(tls.Client)。 - REALITY:如果
reality.ConfigFromStreamSettings返回非 nil(且未配置 TLS),则通过reality.UClient执行 REALITY 握手。
头部伪装
安全层之后,可选的 ConnectionAuthenticator 包装连接(tcp/dialer.go:95-106):
tcpSettings := streamSettings.ProtocolSettings.(*Config)
if tcpSettings.HeaderSettings != nil {
headerConfig, _ := tcpSettings.HeaderSettings.GetInstance()
auth, _ := internet.CreateConnectionAuthenticator(headerConfig)
conn = auth.Client(conn)
}这实现了 HTTP 头部伪装,将 TCP 流包装成看起来像正常 HTTP 流量。
监听流程
入口点
tcp.ListenTCP(tcp/hub.go:30-95)创建 TCP 监听器:
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)接受连接并应用安全层:
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):
streamSettings.SocketSettings.AcceptProxyProtocol =
l.config.AcceptProxyProtocol || streamSettings.SocketSettings.AcceptProxyProtocol启用后,internet.ListenSystem 使用 proxyproto.Listener 包装监听器(system_listener.go:170-173)。
系统拨号器:套接字选项
DefaultSystemDialer.Dial(transport/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_MARK | SOL_SOCKET, SO_MARK | config.Mark |
| SO_BINDTODEVICE | BindToDevice | config.Interface |
| TCP_FASTOPEN_CONNECT | SOL_TCP, TCP_FASTOPEN_CONNECT | config.Tfo(出站) |
| TCP_FASTOPEN | SOL_TCP, TCP_FASTOPEN | config.Tfo(入站) |
| TCP_CONGESTION | SOL_TCP, TCP_CONGESTION | config.TcpCongestion |
| TCP_WINDOW_CLAMP | IPPROTO_TCP, TCP_WINDOW_CLAMP | config.TcpWindowClamp |
| TCP_USER_TIMEOUT | IPPROTO_TCP, TCP_USER_TIMEOUT | config.TcpUserTimeout |
| TCP_MAXSEG | IPPROTO_TCP, TCP_MAXSEG | config.TcpMaxSeg |
| IP_TRANSPARENT | SOL_IP, IP_TRANSPARENT | config.Tproxy |
| IPV6_RECVORIGDSTADDR | 多种 | config.ReceiveOriginalDestAddress |
| 自定义 | 用户定义的 level/opt | config.CustomSockopt |
macOS 套接字选项(sockopt_darwin.go)
Darwin 实现(transport/internet/sockopt_darwin.go:103-192)有平台特定的差异:
- TCP_FASTOPEN 使用平台常量:
TCP_FASTOPEN_SERVER = 0x01,TCP_FASTOPEN_CLIENT = 0x02(sockopt_darwin.go:19-21) - 接口绑定 使用
IP_BOUND_IF/IPV6_BOUND_IF替代SO_BINDTODEVICE(sockopt_darwin.go:136-151) - 透明代理 使用
/dev/pf和DIOCNATLOOKioctl 进行原始目的地查找(sockopt_darwin.go:40-101)
TFO 值解析
TFO 配置值有特殊的解析方式(transport/internet/sockopt.go:21-30):
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 调用:
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 时,TcpRaceDial(transport/internet/happy_eyeballs.go:16-97)实现 RFC 8305:
IP 排序
sortIPs(happy_eyeballs.go:100-156)交错排列 IPv4 和 IPv6 地址:
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, ...])。
竞速拨号算法
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_TRANSPARENT(sockopt_linux.go:131-135)来工作,允许进程绑定到非本地地址。原始目的地恢复使用 SO_ORIGINAL_DST(tcp/sockopt_linux.go:18-52):
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.Listen(transport/internet/system_listener.go:78-175)创建操作系统级别的监听器,包含:
- 套接字控制:通过
getControlFunc(system_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.Conn(system_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 后,监听器会启动一个协程用于
DetectPostHandshakeRecordsLens(tcp/hub.go:78)。