Skip to content

نقل gRPC

مقدمة

يقوم نقل gRPC بنقل حركة مرور البروكسي عبر HTTP/2 باستخدام إطار عمل gRPC. يتم تغليف البيانات في رسائل Hunk المعرّفة بـ protobuf وإرسالها عبر استدعاءات 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"  (client multi mode)

هذا يسمح للمشغلين بإخفاء حركة مرور gRPC كأي خدمة gRPC عشوائية.

تدفق الاتصال

تجميع الاتصالات

getGrpcClient (grpc/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. التراجع الأسي: تراجع أسي يبدأ من 500 مللي ثانية، بحد أقصى 19 ثانية، اهتزاز 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: يُعيّن من الإعدادات، أو ServerName من TLS، أو نطاق الوجهة (dial.go:150-158)
  5. Keepalive: ClientParameters اختيارية مع مهلة خمول قابلة للتكوين، ومهلة فحص صحي، والسماح بدون تدفق (dial.go:160-166)
  6. حجم النافذة الأولي: نافذة تحكم تدفق gRPC اختيارية (dial.go:168-170)
  7. تجاوز User-Agent: يستخدم الانعكاس لتعيين وكيل المستخدم، مع إزالة اللاحقة الافتراضية grpc-go/version (dial.go:184-201)

إنشاء التدفق

dialgRPC (grpc/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.Listen (grpc/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...)
    // Register with custom names:
    encoding.RegisterGRPCServiceServerX(s, listener,
        grpcSettings.getServiceName(),
        grpcSettings.getTunStreamName(),
        grpcSettings.getTunMultiStreamName())
    // ...
    s.Serve(streamListener)
}

معالجات التدفق

بنية Listener تنفذ GRPCServiceServer (grpc/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 حيًا حتى يتم إغلاق الاتصال.

تسجيل الخدمة المخصصة

RegisterGRPCServiceServerX (grpc/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)
}

ServerDesc (customSeviceName.go:9-30) يولّد واصف خدمة مع:

  • ServiceName مخصص
  • تدفقين ثنائيي الاتجاه بأسماء مخصصة
  • كلاهما ServerStreams: true و ClientStreams: 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() next Hunk
    }
    n := copy(buf, h.buf[h.index:])
    h.index += n
    return n, nil
}

الوضع المتعدد (TunMulti)

وضع multi يجمع عدة مخازن مؤقتة في رسالة 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
    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):

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

هذا يتجاوز TLS المدمج في gRPC، باستخدام insecure.NewCredentials() على مستوى gRPC.

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/البروكسي العكسي. الأولوية: إعداد صريح > ServerName من TLS > نطاق الوجهة (dial.go:150-158).
  • خدعة User-Agent: يقوم gRPC-Go بإلحاق grpc-go/<version> بوكيل المستخدم بشكل غير مشروط. يستخدم Xray reflect + 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 للرؤوس.

تحليل تقني لأغراض إعادة التنفيذ.