gRPC 传输
简介
gRPC 传输通过 gRPC 框架将代理流量隧道化在 HTTP/2 之上。数据被封装在 protobuf 定义的 Hunk 消息中,通过双向流式 RPC 发送。该传输支持自定义服务名/流名(伪装成合法的 gRPC 服务)、批量发送多缓冲区的"multi"模式、连接池、keepalive 以及 authority 头部控制。它与 TLS、REALITY 和 uTLS 指纹兼容。
协议注册
以 "grpc" 名称注册(transport/internet/grpc/grpc.go:3):
const protocolName = "grpc"- 拨号器:
grpc/dial.go:37-39 - 监听器:
grpc/hub.go:137-139 - 配置:
grpc/config.go:11-15
服务名架构
旧样式(默认)
当 ServiceName 不以 / 开头时,被视为经典 gRPC 服务名。流名默认为 "Tun" 和 "TunMulti":
/GunService/Tun (单缓冲区模式)
/GunService/TunMulti (多缓冲区模式)新自定义路径样式
当 ServiceName 以 / 开头时,被解析为完整的自定义路径(grpc/config.go:17-59):
// ServiceName = "/my/custom/path/StreamA|StreamB"
// serviceName = "my/custom/path"
// tunStreamName = "StreamA"
// tunMultiStreamName = "StreamB"格式为:/<服务路径>/<tun 名称>|<tun_multi 名称>
客户端在 multi 模式下,直接使用完整路径(不分割 |):
// ServiceName = "/my/custom/path/StreamB" (客户端 multi 模式)这使得运维人员可以将 gRPC 流量伪装成任意 gRPC 服务。
拨号流程
连接池
getGrpcClient(grpc/dial.go:77-193)管理全局 grpc.ClientConn 对象池:
var (
globalDialerMap map[dialerConf]*grpc.ClientConn
globalDialerAccess sync.Mutex
)连接以 {Destination, MemoryStreamConfig} 为键。除非状态为 connectivity.Shutdown,否则复用现有连接(dial.go:89-91)。
客户端连接设置
创建新的 gRPC 客户端连接时(grpc/dial.go:93-193):
- 退避:指数退避,起始 500ms,最大 19s,抖动 0.2(
dial.go:94-102) - 上下文拨号器:自定义
grpc.WithContextDialer:- 调用
internet.DialSystem获取原始 TCP 连接 - 如已配置则应用 TLS(标准或 uTLS)
- 如已配置则应用 REALITY
- 传递出站会话上下文(
dial.go:103-146)
- 调用
- 不安全凭证:始终使用
grpc.WithTransportCredentials(insecure.NewCredentials()),因为 TLS 在原始连接层处理,而非通过 gRPC 的凭证系统(dial.go:148) - Authority:从配置、TLS ServerName 或目标域名设置(
dial.go:150-158) - Keepalive:可选的
ClientParameters,可配置空闲超时、健康检查超时和无流许可(dial.go:160-166) - 初始窗口大小:可选的 gRPC 流控窗口(
dial.go:168-170) - User-Agent 覆盖:使用反射设置 user-agent,移除默认的
grpc-go/version后缀(dial.go:184-201)
流建立
dialgRPC(grpc/dial.go:51-75)打开相应的流:
func dialgRPC(ctx context.Context, dest net.Destination,
streamSettings *internet.MemoryStreamConfig) (net.Conn, error) {
grpcSettings := streamSettings.ProtocolSettings.(*Config)
conn, _ := getGrpcClient(ctx, dest, streamSettings)
client := encoding.NewGRPCServiceClient(conn)
if grpcSettings.MultiMode {
grpcService, _ := client.(encoding.GRPCServiceClientX).TunMultiCustomName(
ctx, grpcSettings.getServiceName(), grpcSettings.getTunMultiStreamName())
return encoding.NewMultiHunkConn(grpcService, nil), nil
}
grpcService, _ := client.(encoding.GRPCServiceClientX).TunCustomName(
ctx, grpcSettings.getServiceName(), grpcSettings.getTunStreamName())
return encoding.NewHunkConn(grpcService, nil), nil
}监听流程
服务器设置
grpc.Listen(grpc/hub.go:53-135)创建 gRPC 服务器:
func Listen(ctx context.Context, address net.Address, port net.Port,
settings *internet.MemoryStreamConfig, handler internet.ConnHandler) (internet.Listener, error) {
// ...
s = grpc.NewServer(options...)
// 以自定义名称注册:
encoding.RegisterGRPCServiceServerX(s, listener,
grpcSettings.getServiceName(),
grpcSettings.getTunStreamName(),
grpcSettings.getTunMultiStreamName())
// ...
s.Serve(streamListener)
}流处理器
Listener 结构体实现 GRPCServiceServer(grpc/hub.go:20-42):
func (l Listener) Tun(server encoding.GRPCService_TunServer) error {
tunCtx, cancel := context.WithCancel(l.ctx)
l.handler(encoding.NewHunkConn(server, cancel))
<-tunCtx.Done()
return nil
}
func (l Listener) TunMulti(server encoding.GRPCService_TunMultiServer) error {
tunCtx, cancel := context.WithCancel(l.ctx)
l.handler(encoding.NewMultiHunkConn(server, cancel))
<-tunCtx.Done()
return nil
}处理器阻塞在 tunCtx.Done() 上,保持 gRPC 流存活直到连接关闭。
自定义服务注册
RegisterGRPCServiceServerX(grpc/encoding/customSeviceName.go:57-60)创建自定义 grpc.ServiceDesc:
func RegisterGRPCServiceServerX(s *grpc.Server, srv GRPCServiceServer,
name, tun, tunMulti string) {
desc := ServerDesc(name, tun, tunMulti)
s.RegisterService(&desc, srv)
}ServerDesc(customSeviceName.go:9-30)生成服务描述符,包含:
- 自定义
ServiceName - 两个具有自定义名称的双向流
- 均设置
ServerStreams: true和ClientStreams: true
线格式
Protobuf 消息
message Hunk {
bytes data = 1;
}
message MultiHunk {
repeated bytes data = 1;
}单缓冲区模式(Tun)
每次 Write 调用发送一个包含数据字节的 Hunk:
// encoding/hunkconn.go:131-141
func (h *HunkReaderWriter) Write(buf []byte) (int, error) {
err := h.hc.Send(&Hunk{Data: buf[:]})
return len(buf), nil
}读取时逐个获取 Hunk 并从其 Data 字段复制:
// encoding/hunkconn.go:91-105
func (h *HunkReaderWriter) Read(buf []byte) (int, error) {
if h.index >= len(h.buf) {
h.forceFetch() // Recv() 获取下一个 Hunk
}
n := copy(buf, h.buf[h.index:])
h.index += n
return n, nil
}多缓冲区模式(TunMulti)
多缓冲区模式在单个 gRPC 消息中批量发送多个缓冲区:
// encoding/multiconn.go:115-134
func (h *MultiHunkReaderWriter) WriteMultiBuffer(mb buf.MultiBuffer) error {
hunks := make([][]byte, 0, len(mb))
for _, b := range mb {
if b.Len() > 0 {
hunks = append(hunks, b.Bytes())
}
}
h.hc.Send(&MultiHunk{Data: hunks})
}当多个小写入被批量处理时,这减少了每条消息的开销。
网络流程
sequenceDiagram
participant Client as 客户端
participant gRPC Client as gRPC 客户端
participant HTTP/2
participant gRPC Server as gRPC 服务端
participant Server as 服务端
Client->>gRPC Client: Write(data)
gRPC Client->>HTTP/2: DATA 帧 (Hunk{data})
HTTP/2->>gRPC Server: DATA 帧
gRPC Server->>Server: Read() -> data
Server->>gRPC Server: Write(response)
gRPC Server->>HTTP/2: DATA 帧 (Hunk{response})
HTTP/2->>gRPC Client: DATA 帧
gRPC Client->>Client: Read() -> response连接包装
HunkConn
NewHunkConn(encoding/hunkconn.go:41-73)将 gRPC 流包装为 net.Conn:
- 使用
common/net/cnc中的cnc.NewConnection构建net.Conn - 从 gRPC 的
peer.FromContext提取远程地址 - 支持通过
x-real-ip元数据头部传递真实 IP
MultiHunkConn
NewMultiHunkConn(encoding/multiconn.go:37-69)类似,但使用 ConnectionInputMulti/ConnectionOutputMulti 进行批量缓冲区操作。
两种类型均实现 StreamCloser 以支持 CloseSend() 来通知客户端流结束。
TLS 与安全
客户端 TLS
TLS 在上下文拨号器中的原始连接层处理(grpc/dial.go:128-143):
if tlsConfig != nil {
config := tlsConfig.GetTLSConfig()
if fingerprint := tls.GetFingerprint(tlsConfig.Fingerprint); fingerprint != nil {
return tls.UClient(c, config, fingerprint), nil
} else {
return tls.Client(c, config), nil
}
}
if realityConfig != nil {
return reality.UClient(c, realityConfig, gctx, dest)
}这绕过了 gRPC 内置的 TLS,在 gRPC 层使用 insecure.NewCredentials()。
服务端 TLS
在服务端,TLS 的处理方式不同 —— 通过 gRPC 的凭证系统(grpc/hub.go:82-85):
if config != nil {
options = append(options, grpc.Creds(credentials.NewTLS(
config.GetTLSConfig(tls.WithNextProto("h2")))))
}REALITY 通过包装监听器处理(hub.go:126-128):
if config := reality.ConfigFromStreamSettings(settings); config != nil {
streamListener = goreality.NewListener(streamListener, config.GetREALITYConfig())
}实现说明
- 连接复用:gRPC 在单个 HTTP/2 连接上多路复用流。
globalDialerMap缓存ClientConn对象以避免每个新代理连接都重新连接。 - Authority 头部:对于 CDN/反向代理场景至关重要。优先级:显式配置 > TLS ServerName > 目标域名(
dial.go:150-158)。 - User-Agent 技巧:gRPC-Go 无条件地在 user-agent 后追加
grpc-go/<version>。Xray 使用reflect+unsafe.Pointer覆盖此值(dial.go:197-201),默认为 Chrome user-agent 字符串。 - URL 编码名称:服务名和流名经过 URL 路径转义以确保有效的 gRPC 路径(
config.go:17-58)。 - passthrough 解析器:
grpc.NewClient调用使用passthrough:///scheme 禁用 gRPC 的 DNS 解析,因为 Xray 自行处理解析(dial.go:179-180)。 - 服务端阻塞:
Tun/TunMulti处理器阻塞在tunCtx.Done()上。当HunkReaderWriter关闭时上下文被取消,解除处理器阻塞并结束 gRPC 流。 - 无头部伪装:与 TCP 传输不同,gRPC 不支持
ConnectionAuthenticator头部包装。