Skip to content

gRPC 传输

简介

gRPC 传输通过 gRPC 框架将代理流量隧道化在 HTTP/2 之上。数据被封装在 protobuf 定义的 Hunk 消息中,通过双向流式 RPC 发送。该传输支持自定义服务名/流名(伪装成合法的 gRPC 服务)、批量发送多缓冲区的"multi"模式、连接池、keepalive 以及 authority 头部控制。它与 TLS、REALITY 和 uTLS 指纹兼容。

协议注册

"grpc" 名称注册(transport/internet/grpc/grpc.go:3):

go
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):

go
// 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 服务。

拨号流程

连接池

getGrpcClientgrpc/dial.go:77-193)管理全局 grpc.ClientConn 对象池:

go
var (
    globalDialerMap    map[dialerConf]*grpc.ClientConn
    globalDialerAccess sync.Mutex
)

连接以 {Destination, MemoryStreamConfig} 为键。除非状态为 connectivity.Shutdown,否则复用现有连接(dial.go:89-91)。

客户端连接设置

创建新的 gRPC 客户端连接时(grpc/dial.go:93-193):

  1. 退避:指数退避,起始 500ms,最大 19s,抖动 0.2(dial.go:94-102
  2. 上下文拨号器:自定义 grpc.WithContextDialer
    • 调用 internet.DialSystem 获取原始 TCP 连接
    • 如已配置则应用 TLS(标准或 uTLS)
    • 如已配置则应用 REALITY
    • 传递出站会话上下文(dial.go:103-146
  3. 不安全凭证:始终使用 grpc.WithTransportCredentials(insecure.NewCredentials()),因为 TLS 在原始连接层处理,而非通过 gRPC 的凭证系统(dial.go:148
  4. Authority:从配置、TLS ServerName 或目标域名设置(dial.go:150-158
  5. Keepalive:可选的 ClientParameters,可配置空闲超时、健康检查超时和无流许可(dial.go:160-166
  6. 初始窗口大小:可选的 gRPC 流控窗口(dial.go:168-170
  7. User-Agent 覆盖:使用反射设置 user-agent,移除默认的 grpc-go/version 后缀(dial.go:184-201

流建立

dialgRPCgrpc/dial.go:51-75)打开相应的流:

go
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.Listengrpc/hub.go:53-135)创建 gRPC 服务器:

go
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 结构体实现 GRPCServiceServergrpc/hub.go:20-42):

go
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 流存活直到连接关闭。

自定义服务注册

RegisterGRPCServiceServerXgrpc/encoding/customSeviceName.go:57-60)创建自定义 grpc.ServiceDesc

go
func RegisterGRPCServiceServerX(s *grpc.Server, srv GRPCServiceServer,
    name, tun, tunMulti string) {
    desc := ServerDesc(name, tun, tunMulti)
    s.RegisterService(&desc, srv)
}

ServerDesccustomSeviceName.go:9-30)生成服务描述符,包含:

  • 自定义 ServiceName
  • 两个具有自定义名称的双向流
  • 均设置 ServerStreams: trueClientStreams: true

线格式

Protobuf 消息

protobuf
message Hunk {
    bytes data = 1;
}

message MultiHunk {
    repeated bytes data = 1;
}

单缓冲区模式(Tun)

每次 Write 调用发送一个包含数据字节的 Hunk

go
// 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 字段复制:

go
// 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 消息中批量发送多个缓冲区:

go
// 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})
}

当多个小写入被批量处理时,这减少了每条消息的开销。

网络流程

mermaid
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

NewHunkConnencoding/hunkconn.go:41-73)将 gRPC 流包装为 net.Conn

  • 使用 common/net/cnc 中的 cnc.NewConnection 构建 net.Conn
  • 从 gRPC 的 peer.FromContext 提取远程地址
  • 支持通过 x-real-ip 元数据头部传递真实 IP

MultiHunkConn

NewMultiHunkConnencoding/multiconn.go:37-69)类似,但使用 ConnectionInputMulti/ConnectionOutputMulti 进行批量缓冲区操作。

两种类型均实现 StreamCloser 以支持 CloseSend() 来通知客户端流结束。

TLS 与安全

客户端 TLS

TLS 在上下文拨号器中的原始连接层处理(grpc/dial.go:128-143):

go
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):

go
if config != nil {
    options = append(options, grpc.Creds(credentials.NewTLS(
        config.GetTLSConfig(tls.WithNextProto("h2")))))
}

REALITY 通过包装监听器处理(hub.go:126-128):

go
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 头部包装。

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