gRPC
Tunnel over HTTP/2 streams that look like ordinary gRPC service calls.
gRPC is the trick of hiding a proxy inside a protocol that data centres already run at scale. To a
middlebox the traffic is an ordinary gRPC service — HTTP/2 requests to a method like /GunService/Tun, carrying length-delimited protobuf messages. Nothing about the shape says
“tunnel”. And because gRPC is built on HTTP/2, you get connection multiplexing for free: many proxy
connections ride one TCP socket without any of Xray’s own machinery.
Every app connection is a stream, and they all share one connection
Opening a proxy connection over gRPC does not open a new TCP connection — it opens a new HTTP/2
stream on a connection Xray already has. The dialer keeps a globalDialerMap of one grpc.ClientConn per dialerConf — the (destination, stream settings) pair — and reuses it; each Dial just calls NewStream on that shared client.
// getGrpcClient: reuse the cached ClientConn unless it has shut down
if client, found := globalDialerMap[dialerConf{dest, streamSettings}]; found && client.GetState() != connectivity.Shutdown {
return client, nil
}
// ...otherwise dial once and cache it
globalDialerMap[dialerConf{dest, streamSettings}] = conn The service name is the URL path
serviceName is not metadata — it is the literal HTTP/2 :path the request is sent to. The
client builds the path as /serviceName/Tun and dials that method; the server registers a gRPC
service under the same name. Get it wrong on either side and the server returns “unimplemented”.
// client stream is opened against "/" + name + "/" + tun
stream, err := c.cc.NewStream(ctx, &ServerDesc(name, tun, "").Streams[0], "/"+name+"/"+tun, opts...) With "serviceName": "GunService" the tunnel connection is a POST to /GunService/Tun — which is
exactly what a real gRPC client would send. getServiceName URL-escapes the value; a normal name
gets the default stream names Tun and TunMulti. A serviceName that starts with / switches to
custom-path mode, where the segment after the last / becomes the method name — split on | when
the server needs distinct names for Tun and TunMulti (getTunStreamName takes the part before
the |, getTunMultiStreamName the part after). That lets you disguise the tunnel as any deep API
path a CDN expects.
{
"network": "grpc",
"grpcSettings": {
"serviceName": "GunService",
"multiMode": false
}
} Tun ships one buffer per message; TunMulti ships a batch
The service exposes two bidirectional streaming methods, and multiMode picks which one you
tunnel over. Both are stream ... returns (stream ...), but they carry different protobuf
messages.
service GRPCService {
rpc Tun (stream Hunk) returns (stream Hunk);
rpc TunMulti (stream MultiHunk) returns (stream MultiHunk);
}
message Hunk { bytes data = 1; } // one chunk per frame
message MultiHunk { repeated bytes data = 1; } // many chunks in one frame Plain Tun sends a fresh gRPC message for every write. TunMulti (enabled with "multiMode": true) packs a whole MultiBuffer — every pending buffer — into a single MultiHunk,
so one gRPC frame can flush several chunks at once. That cuts per-message protobuf and HTTP/2 framing
overhead when the proxy is moving many small buffers. The two methods are wire-incompatible — but the
server registers both handlers under the configured names (RegisterGRPCServiceServerX in hub.go)
and never reads its own multiMode, so the switch is purely a client-side choice. What must match on
both sides is serviceName, because it determines the paths both methods live at.
It only works if HTTP/2 survives end to end
gRPC is not just “HTTP/2-flavoured” — it needs real, un-downgraded HTTP/2 with trailers all the way
to the server. The listener forces the h2 ALPN protocol on its TLS config, and the whole scheme
falls apart if anything in the path terminates or rewrites the connection as HTTP/1.
// gRPC server may silently ignore TLS errors
options = append(options, grpc.Creds(credentials.NewTLS(config.GetTLSConfig(tls.WithNextProto("h2"))))) That “silently ignore TLS errors” comment is not idle: a misconfigured front can make the H2 stream fail as a bare reset rather than a clear error. gRPC also relies on HTTP/2 trailers to close a call, which some CDNs strip or mishandle — the reason gRPC only partially survives CDN fronting.
Front gRPC with a CDN that lacks gRPC support
A CDN that terminates HTTP/2 as HTTP/1.1, buffers responses, or drops trailers will kill the
long-lived Tun stream — often as an abrupt reset mid-transfer with no useful log line.
Terminate TLS at a gRPC-aware front, or go direct
Point clients straight at the server, or use a front that proxies HTTP/2 end to end (gRPC
passthrough). Keep the same serviceName on both sides so the :path matches.