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 للرؤوس.

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