نقل gRPC
مقدمة
يقوم نقل gRPC بنقل حركة مرور البروكسي عبر HTTP/2 باستخدام إطار عمل gRPC. يتم تغليف البيانات في رسائل Hunk المعرّفة بـ protobuf وإرسالها عبر استدعاءات 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" (client multi mode)هذا يسمح للمشغلين بإخفاء حركة مرور 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):
- التراجع الأسي: تراجع أسي يبدأ من 500 مللي ثانية، بحد أقصى 19 ثانية، اهتزاز 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: يُعيّن من الإعدادات، أو ServerName من TLS، أو نطاق الوجهة (
dial.go:150-158) - Keepalive:
ClientParametersاختيارية مع مهلة خمول قابلة للتكوين، ومهلة فحص صحي، والسماح بدون تدفق (dial.go:160-166) - حجم النافذة الأولي: نافذة تحكم تدفق gRPC اختيارية (
dial.go:168-170) - تجاوز 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...)
// Register with custom names:
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() next Hunk
}
n := copy(buf, h.buf[h.index:])
h.index += n
return n, nil
}الوضع المتعدد (TunMulti)
وضع multi يجمع عدة مخازن مؤقتة في رسالة 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
participant gRPC Client
participant HTTP/2
participant gRPC Server
participant Server
Client->>gRPC Client: Write(data)
gRPC Client->>HTTP/2: DATA frame (Hunk{data})
HTTP/2->>gRPC Server: DATA frame
gRPC Server->>Server: Read() -> data
Server->>gRPC Server: Write(response)
gRPC Server->>HTTP/2: DATA frame (Hunk{response})
HTTP/2->>gRPC Client: DATA frame
gRPC Client->>Client: Read() -> responseتغليف الاتصال
HunkConn
NewHunkConn (encoding/hunkconn.go:41-73) يغلف تدفق gRPC كـ net.Conn:
- يستخدم
cnc.NewConnectionمنcommon/net/cncلبناء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)
}هذا يتجاوز TLS المدمج في gRPC، باستخدام insecure.NewCredentials() على مستوى gRPC.
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/البروكسي العكسي. الأولوية: إعداد صريح > ServerName من TLS > نطاق الوجهة (
dial.go:150-158). - خدعة User-Agent: يقوم gRPC-Go بإلحاق
grpc-go/<version>بوكيل المستخدم بشكل غير مشروط. يستخدم Xrayreflect+unsafe.Pointerلاستبدال هذا (dial.go:197-201)، مع استخدام سلسلة وكيل مستخدم Chrome كقيمة افتراضية. - الأسماء المشفرة بـ URL: يتم تشفير أسماء الخدمة والتدفق بترميز مسار URL لضمان مسارات gRPC صالحة (
config.go:17-58). - مُحلّل passthrough: استدعاء
grpc.NewClientيستخدم مخططpassthrough:///لتعطيل حل DNS الخاص بـ gRPC، لأن Xray يتعامل مع الحل بنفسه (dial.go:179-180). - حجب الخادم: معالجات
Tun/TunMultiتحجب علىtunCtx.Done(). يتم إلغاء السياق عند إغلاقHunkReaderWriter، مما يفك حجب المعالج وينهي تدفق gRPC. - بدون تمويه رؤوس: على عكس نقل TCP، لا يدعم gRPC تغليف
ConnectionAuthenticatorللرؤوس.