Skip to content

نقل gRPC ​

المصدر: transport/v2raygrpc/، transport/v2raygrpclite/، transport/v2ray/grpc.go، transport/v2ray/grpc_lite.go

نظرة عامة ​

يوفر sing-box تنفيذين لـ gRPC:

  1. gRPC كامل (v2raygrpc): يستخدم google.golang.org/grpc، يتطلب علامة البناء with_grpc
  2. gRPC خفيف (v2raygrpclite): تنفيذ HTTP/2 خام باستخدام golang.org/x/net/http2، متاح دائمًا

كلاهما يُنفِّذ بروتوكول V2Ray "Gun" -- خدمة gRPC ثنائية الاتجاه تنقل بيانات TCP عشوائية عبر نفق.

تنفيذ gRPC الكامل ​

العميل ​

go
type Client struct {
    ctx         context.Context
    dialer      N.Dialer
    serverAddr  string
    serviceName string
    dialOptions []grpc.DialOption
    conn        atomic.Pointer[grpc.ClientConn]
    connAccess  sync.Mutex
}

التخزين المؤقت للاتصال يستخدم مؤشرًا ذريًا + قفلًا متبادلًا (قفل مزدوج التحقق):

go
func (c *Client) connect() (*grpc.ClientConn, error) {
    conn := c.conn.Load()
    if conn != nil && conn.GetState() != connectivity.Shutdown {
        return conn, nil
    }
    c.connAccess.Lock()
    defer c.connAccess.Unlock()
    conn = c.conn.Load()  // Re-check after lock
    if conn != nil && conn.GetState() != connectivity.Shutdown {
        return conn, nil
    }
    conn, err := grpc.DialContext(c.ctx, c.serverAddr, c.dialOptions...)
    c.conn.Store(conn)
    return conn, nil
}

تكامل TLS: يستخدم محول TLSTransportCredentials مخصصًا يربط واجهة إعدادات TLS الخاصة بـ sing-box مع credentials.TransportCredentials الخاص بـ gRPC. بدون TLS، يستخدم insecure.NewCredentials().

خيارات الاتصال تتضمن:

  • معاملات البقاء على قيد الحياة (IdleTimeout، PingTimeout، PermitWithoutStream)
  • إعدادات التراجع (أساس 500 مللي ثانية، مُضاعف 1.5، حد أقصى 19 ثانية)
  • طالب اتصال مخصص يربط N.Dialer بـ net.Conn

اسم الخدمة المخصص ​

يستخدم بروتوكول Gun اسم خدمة مخصصًا لمسار دالة gRPC:

go
func ServerDesc(name string) grpc.ServiceDesc {
    return grpc.ServiceDesc{
        ServiceName: name,
        Streams: []grpc.StreamDesc{{
            StreamName:    "Tun",
            Handler:       _GunService_Tun_Handler,
            ServerStreams: true,
            ClientStreams: true,
        }},
        Metadata: "gun.proto",
    }
}

func (c *gunServiceClient) TunCustomName(ctx context.Context, name string, opts ...grpc.CallOption) (GunService_TunClient, error) {
    stream, err := c.cc.NewStream(ctx, &ServerDesc(name).Streams[0], "/"+name+"/Tun", opts...)
    // ...
}

يصبح مسار الدالة /<serviceName>/Tun. اسم الخدمة الافتراضي هو GunService.

الخادم ​

go
func (s *Server) Tun(server GunService_TunServer) error {
    conn := NewGRPCConn(server, nil)
    var source M.Socksaddr
    // Extract source from gRPC peer info
    if remotePeer, loaded := peer.FromContext(server.Context()); loaded {
        source = M.SocksaddrFromNet(remotePeer.Addr)
    }
    // Override with X-Forwarded-For if present (CDN support)
    if grpcMetadata, loaded := gM.FromIncomingContext(server.Context()); loaded {
        forwardFrom := strings.Join(grpcMetadata.Get("X-Forwarded-For"), ",")
        // Parse last valid address from comma-separated list
    }
    done := make(chan struct{})
    go s.handler.NewConnectionEx(log.ContextWithNewID(s.ctx), conn, source, M.Socksaddr{},
        N.OnceClose(func(it error) { close(done) }))
    <-done  // Block until connection handler completes
    return nil
}

GRPCConn ​

يُكيِّف تدفق gRPC ثنائي الاتجاه ليكون net.Conn:

go
type GRPCConn struct {
    GunService          // Send/Recv interface
    cache     []byte    // Buffered data from oversized Recv
    cancel    context.CancelCauseFunc
    closeOnce sync.Once
}

func (c *GRPCConn) Read(b []byte) (n int, err error) {
    if len(c.cache) > 0 {
        n = copy(b, c.cache)
        c.cache = c.cache[n:]
        return
    }
    hunk, err := c.Recv()
    n = copy(b, hunk.Data)
    if n < len(hunk.Data) {
        c.cache = hunk.Data[n:]
    }
    return
}

تحتوي رسالة Protobuf Hunk على حقل Data واحد. عندما يكون مخزن القراءة أصغر من القطعة المُستقبَلة، تُخزَّن البيانات الزائدة مؤقتًا للقراءات التالية.

تنفيذ gRPC الخفيف ​

صيغة السلك (بروتوكول Gun) ​

يبني التنفيذ الخفيف صيغة سلك Gun يدويًا فوق HTTP/2:

[0x00][4-byte big-endian frame length][0x0A][varint data length][data]

حيث:

  • 0x00: علامة ضغط gRPC (غير مضغوط دائمًا)
  • طول الإطار: 1 + varint_length + data_length
  • 0x0A: وسم حقل Protobuf (الحقل 1، نوع السلك 2 = محدد الطول)
  • طول بيانات Varint: ترميز varint قياسي لـ Protobuf لطول البيانات
go
func (c *GunConn) Write(b []byte) (n int, err error) {
    varLen := varbin.UvarintLen(uint64(len(b)))
    buffer := buf.NewSize(6 + varLen + len(b))
    header := buffer.Extend(6 + varLen)
    header[0] = 0x00
    binary.BigEndian.PutUint32(header[1:5], uint32(1+varLen+len(b)))
    header[5] = 0x0A
    binary.PutUvarint(header[6:], uint64(len(b)))
    common.Must1(buffer.Write(b))
    _, err = c.writer.Write(buffer.Bytes())
    if c.flusher != nil {
        c.flusher.Flush()
    }
    return len(b), nil
}

تتجاهل القراءة أول 6 بايتات (علامة الضغط + طول الإطار + وسم Protobuf)، ثم تقرأ طول بيانات varint، ثم تبث الحمولة:

go
func (c *GunConn) read(b []byte) (n int, err error) {
    if c.readRemaining > 0 {
        // Continue reading from current frame
    }
    _, err = c.reader.Discard(6)
    dataLen, err := binary.ReadUvarint(c.reader)
    c.readRemaining = int(dataLen)
    // Read up to readRemaining bytes
}

العميل الخفيف ​

يستخدم http2.Transport مباشرة مع io.Pipe للتدفق ثنائي الاتجاه:

go
func (c *Client) DialContext(ctx context.Context) (net.Conn, error) {
    pipeInReader, pipeInWriter := io.Pipe()
    request := &http.Request{
        Method: http.MethodPost,
        Body:   pipeInReader,
        URL:    c.url,  // /<serviceName>/Tun
        Header: defaultClientHeader,  // Content-Type: application/grpc
    }
    conn := newLateGunConn(pipeInWriter)
    go func() {
        response, err := c.transport.RoundTrip(request)
        conn.setup(response.Body, err)
    }()
    return conn, nil
}

ترويسات العميل الافتراضية:

go
var defaultClientHeader = http.Header{
    "Content-Type": []string{"application/grpc"},
    "User-Agent":   []string{"grpc-go/1.48.0"},
    "TE":           []string{"trailers"},
}

الخادم الخفيف ​

يتحقق من متطلبات gRPC المحددة:

go
func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
    // Handle h2c preface
    if request.Method == "PRI" && len(request.Header) == 0 && request.URL.Path == "*" {
        s.h2cHandler.ServeHTTP(writer, request)
        return
    }
    // Validate path: /<serviceName>/Tun
    // Validate method: POST
    // Validate content-type: application/grpc
    writer.Header().Set("Content-Type", "application/grpc")
    writer.Header().Set("TE", "trailers")
    writer.WriteHeader(http.StatusOK)
    conn := v2rayhttp.NewHTTP2Wrapper(newGunConn(request.Body, writer, writer.(http.Flusher)))
    s.handler.NewConnectionEx(...)
}

يدعم الخادم كلاً من TLS (h2) و النص الواضح (h2c) لـ HTTP/2. يكتشف معالج h2c مقدمة اتصال HTTP/2 (PRI * HTTP/2.0).

المساحة الاحتياطية الأمامية ​

يُعلن GunConn الخفيف عن مساحة احتياطية أمامية للكتابات بدون نسخ:

go
func (c *GunConn) FrontHeadroom() int {
    return 6 + binary.MaxVarintLen64  // 6 + 10 = 16 bytes
}

الإعدادات ​

json
{
  "transport": {
    "type": "grpc",
    "service_name": "TunService",
    "idle_timeout": "15s",
    "ping_timeout": "15s",
    "permit_without_stream": false,
    "force_lite": false
  }
}
الحقلالوصف
service_nameاسم خدمة gRPC لمسار الدالة /<name>/Tun
idle_timeoutمهلة الخمول للبقاء على قيد الحياة
ping_timeoutمهلة ping للبقاء على قيد الحياة
permit_without_streamالسماح بنبضات البقاء على قيد الحياة بدون تدفقات نشطة (gRPC الكامل فقط)
force_liteفرض التنفيذ الخفيف حتى مع علامة البناء with_grpc