Contents

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.

transport/internet/grpc/dial.go transport/internet/grpc/encoding/customSeviceName.go
Three app connections become three HTTP/2 streams over one reused TCP + TLS connection.
// 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”.

transport/internet/grpc/config.go transport/internet/grpc/encoding/customSeviceName.go
// 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.

transport/internet/grpc/encoding/stream.proto transport/internet/grpc/encoding/multiconn.go
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.

transport/internet/grpc/hub.go
// 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.

Don't

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.

Do

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.