Browse Source

Grpc Gun Transport (#757)

* introduce grpc transport structure

* fix package name inconsistency

* grpc gun transport dialer and listener

* add selective build tag

* add grpc:gun listener

* add grpc:gun config

* add generated files

* various bug fix for gun:grpc transport

* Cache dialed connections

* grpc:gun Use V2Ray Managed Dial function

* Update destination.pb.go

* Update gun.go

* GunSettings -> GunConfig

* gu -> gs

* add grpc alias

Co-authored-by: RPRX <63339210+rprx@users.noreply.github.com>
Co-authored-by: kslr <kslrwang@gmail.com>
Xiaokang Wang 4 years ago
parent
commit
aaa9e788e7

+ 14 - 0
infra/conf/gun.go

@@ -0,0 +1,14 @@
+package conf
+
+import (
+	"github.com/golang/protobuf/proto"
+	"github.com/v2fly/v2ray-core/v4/transport/internet/grpc"
+)
+
+type GunConfig struct {
+	ServiceName string `json:"serviceName"`
+}
+
+func (g GunConfig) Build() (proto.Message, error) {
+	return &grpc.Config{ServiceName: g.ServiceName}, nil
+}

+ 12 - 0
infra/conf/transport.go

@@ -13,6 +13,7 @@ type TransportConfig struct {
 	HTTPConfig *HTTPConfig         `json:"httpSettings"`
 	DSConfig   *DomainSocketConfig `json:"dsSettings"`
 	QUICConfig *QUICConfig         `json:"quicSettings"`
+	GunConfig  *GunConfig          `json:"gunSettings"`
 }
 
 // Build implements Buildable.
@@ -85,5 +86,16 @@ func (c *TransportConfig) Build() (*transport.Config, error) {
 		})
 	}
 
+	if c.GunConfig != nil {
+		gs, err := c.GunConfig.Build()
+		if err != nil {
+			return nil, newError("Failed to build Gun config.").Base(err)
+		}
+		config.TransportSettings = append(config.TransportSettings, &internet.TransportConfig{
+			ProtocolName: "gun",
+			Settings:     serial.ToTypedMessage(gs),
+		})
+	}
+
 	return config, nil
 }

+ 4 - 0
infra/conf/transport_internet.go

@@ -336,6 +336,10 @@ func (p TransportProtocol) Build() (string, error) {
 		return "domainsocket", nil
 	case "quic":
 		return "quic", nil
+	case "gun":
+		return "gun", nil
+	case "grpc": // gun alias
+		return "gun", nil
 	default:
 		return "", newError("Config: unknown transport protocol: ", p)
 	}

+ 1 - 0
main/distro/all/all.go

@@ -42,6 +42,7 @@ import (
 
 	// Transports
 	_ "github.com/v2fly/v2ray-core/v4/transport/internet/domainsocket"
+	_ "github.com/v2fly/v2ray-core/v4/transport/internet/grpc"
 	_ "github.com/v2fly/v2ray-core/v4/transport/internet/http"
 	_ "github.com/v2fly/v2ray-core/v4/transport/internet/kcp"
 	_ "github.com/v2fly/v2ray-core/v4/transport/internet/quic"

+ 14 - 0
transport/internet/grpc/config.go

@@ -0,0 +1,14 @@
+package grpc
+
+import (
+	"github.com/v2fly/v2ray-core/v4/common"
+	"github.com/v2fly/v2ray-core/v4/transport/internet"
+)
+
+const protocolName = "gun"
+
+func init() {
+	common.Must(internet.RegisterProtocolConfigCreator(protocolName, func() interface{} {
+		return new(Config)
+	}))
+}

+ 163 - 0
transport/internet/grpc/config.pb.go

@@ -0,0 +1,163 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// 	protoc-gen-go v1.25.0
+// 	protoc        v3.13.0
+// source: transport/internet/grpc/config.proto
+
+package grpc
+
+import (
+	proto "github.com/golang/protobuf/proto"
+	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+	reflect "reflect"
+	sync "sync"
+)
+
+const (
+	// Verify that this generated code is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+	// Verify that runtime/protoimpl is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+// This is a compile-time assertion that a sufficiently up-to-date version
+// of the legacy proto package is being used.
+const _ = proto.ProtoPackageIsVersion4
+
+type Config struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	Host        string `protobuf:"bytes,1,opt,name=host,proto3" json:"host,omitempty"`
+	ServiceName string `protobuf:"bytes,2,opt,name=service_name,json=serviceName,proto3" json:"service_name,omitempty"`
+}
+
+func (x *Config) Reset() {
+	*x = Config{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_transport_internet_grpc_config_proto_msgTypes[0]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *Config) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*Config) ProtoMessage() {}
+
+func (x *Config) ProtoReflect() protoreflect.Message {
+	mi := &file_transport_internet_grpc_config_proto_msgTypes[0]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use Config.ProtoReflect.Descriptor instead.
+func (*Config) Descriptor() ([]byte, []int) {
+	return file_transport_internet_grpc_config_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *Config) GetHost() string {
+	if x != nil {
+		return x.Host
+	}
+	return ""
+}
+
+func (x *Config) GetServiceName() string {
+	if x != nil {
+		return x.ServiceName
+	}
+	return ""
+}
+
+var File_transport_internet_grpc_config_proto protoreflect.FileDescriptor
+
+var file_transport_internet_grpc_config_proto_rawDesc = []byte{
+	0x0a, 0x24, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x69, 0x6e, 0x74, 0x65,
+	0x72, 0x6e, 0x65, 0x74, 0x2f, 0x67, 0x72, 0x70, 0x63, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67,
+	0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x2b, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f,
+	0x72, 0x65, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74,
+	0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x65, 0x6e, 0x63, 0x6f, 0x64,
+	0x69, 0x6e, 0x67, 0x22, 0x3f, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a,
+	0x04, 0x68, 0x6f, 0x73, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68, 0x6f, 0x73,
+	0x74, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x6e, 0x61, 0x6d,
+	0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65,
+	0x4e, 0x61, 0x6d, 0x65, 0x42, 0x38, 0x5a, 0x36, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63,
+	0x6f, 0x6d, 0x2f, 0x76, 0x32, 0x66, 0x6c, 0x79, 0x2f, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2d, 0x63,
+	0x6f, 0x72, 0x65, 0x2f, 0x76, 0x34, 0x2f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74,
+	0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2f, 0x67, 0x72, 0x70, 0x63, 0x62, 0x06,
+	0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+}
+
+var (
+	file_transport_internet_grpc_config_proto_rawDescOnce sync.Once
+	file_transport_internet_grpc_config_proto_rawDescData = file_transport_internet_grpc_config_proto_rawDesc
+)
+
+func file_transport_internet_grpc_config_proto_rawDescGZIP() []byte {
+	file_transport_internet_grpc_config_proto_rawDescOnce.Do(func() {
+		file_transport_internet_grpc_config_proto_rawDescData = protoimpl.X.CompressGZIP(file_transport_internet_grpc_config_proto_rawDescData)
+	})
+	return file_transport_internet_grpc_config_proto_rawDescData
+}
+
+var file_transport_internet_grpc_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
+var file_transport_internet_grpc_config_proto_goTypes = []interface{}{
+	(*Config)(nil), // 0: v2ray.core.transport.internet.grpc.encoding.Config
+}
+var file_transport_internet_grpc_config_proto_depIdxs = []int32{
+	0, // [0:0] is the sub-list for method output_type
+	0, // [0:0] is the sub-list for method input_type
+	0, // [0:0] is the sub-list for extension type_name
+	0, // [0:0] is the sub-list for extension extendee
+	0, // [0:0] is the sub-list for field type_name
+}
+
+func init() { file_transport_internet_grpc_config_proto_init() }
+func file_transport_internet_grpc_config_proto_init() {
+	if File_transport_internet_grpc_config_proto != nil {
+		return
+	}
+	if !protoimpl.UnsafeEnabled {
+		file_transport_internet_grpc_config_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*Config); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+	}
+	type x struct{}
+	out := protoimpl.TypeBuilder{
+		File: protoimpl.DescBuilder{
+			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+			RawDescriptor: file_transport_internet_grpc_config_proto_rawDesc,
+			NumEnums:      0,
+			NumMessages:   1,
+			NumExtensions: 0,
+			NumServices:   0,
+		},
+		GoTypes:           file_transport_internet_grpc_config_proto_goTypes,
+		DependencyIndexes: file_transport_internet_grpc_config_proto_depIdxs,
+		MessageInfos:      file_transport_internet_grpc_config_proto_msgTypes,
+	}.Build()
+	File_transport_internet_grpc_config_proto = out.File
+	file_transport_internet_grpc_config_proto_rawDesc = nil
+	file_transport_internet_grpc_config_proto_goTypes = nil
+	file_transport_internet_grpc_config_proto_depIdxs = nil
+}

+ 8 - 0
transport/internet/grpc/config.proto

@@ -0,0 +1,8 @@
+syntax = "proto3";
+package v2ray.core.transport.internet.grpc.encoding;
+option go_package = "github.com/v2fly/v2ray-core/v4/transport/internet/grpc";
+
+message Config {
+  string host = 1;
+  string service_name = 2;
+}

+ 107 - 0
transport/internet/grpc/dial.go

@@ -0,0 +1,107 @@
+// +build !confonly
+
+package grpc
+
+import (
+	"context"
+	gonet "net"
+	"sync"
+	"time"
+
+	"github.com/v2fly/v2ray-core/v4/common"
+	"github.com/v2fly/v2ray-core/v4/common/net"
+	"github.com/v2fly/v2ray-core/v4/common/session"
+	"github.com/v2fly/v2ray-core/v4/transport/internet"
+	"github.com/v2fly/v2ray-core/v4/transport/internet/grpc/encoding"
+	"github.com/v2fly/v2ray-core/v4/transport/internet/tls"
+	"google.golang.org/grpc"
+	"google.golang.org/grpc/backoff"
+	"google.golang.org/grpc/connectivity"
+	"google.golang.org/grpc/credentials"
+)
+
+func Dial(ctx context.Context, dest net.Destination, streamSettings *internet.MemoryStreamConfig) (internet.Connection, error) {
+	newError("creating connection to ", dest).WriteToLog(session.ExportIDToError(ctx))
+
+	conn, err := dialgRPC(ctx, dest, streamSettings)
+	if err != nil {
+		return nil, newError("failed to dial Grpc").Base(err)
+	}
+	return internet.Connection(conn), nil
+}
+
+func init() {
+	common.Must(internet.RegisterTransportDialer(protocolName, Dial))
+}
+
+var (
+	globalDialerMap    map[net.Destination]*grpc.ClientConn
+	globalDialerAccess sync.Mutex
+)
+
+func dialgRPC(ctx context.Context, dest net.Destination, streamSettings *internet.MemoryStreamConfig) (net.Conn, error) {
+	grpcSettings := streamSettings.ProtocolSettings.(*Config)
+
+	config := tls.ConfigFromStreamSettings(streamSettings)
+	var dialOption = grpc.WithInsecure()
+
+	if config != nil {
+		dialOption = grpc.WithTransportCredentials(credentials.NewTLS(config.GetTLSConfig()))
+	}
+
+	conn, err := getGrpcClient(dest, dialOption)
+
+	if err != nil {
+		return nil, newError("Cannot dial grpc").Base(err)
+	}
+	client := encoding.NewGunServiceClient(conn)
+	gunService, err := client.(encoding.GunServiceClientX).TunCustomName(ctx, grpcSettings.ServiceName)
+	if err != nil {
+		return nil, newError("Cannot dial grpc").Base(err)
+	}
+	return encoding.NewClientConn(gunService), nil
+}
+
+func getGrpcClient(dest net.Destination, dialOption grpc.DialOption) (*grpc.ClientConn, error) {
+	globalDialerAccess.Lock()
+	defer globalDialerAccess.Unlock()
+
+	if globalDialerMap == nil {
+		globalDialerMap = make(map[net.Destination]*grpc.ClientConn)
+	}
+
+	if client, found := globalDialerMap[dest]; found && client.GetState() != connectivity.Shutdown {
+		return client, nil
+	}
+
+	conn, err := grpc.Dial(
+		dest.Address.String()+":"+dest.Port.String(),
+		dialOption,
+		grpc.WithConnectParams(grpc.ConnectParams{
+			Backoff: backoff.Config{
+				BaseDelay:  500 * time.Millisecond,
+				Multiplier: 1.5,
+				Jitter:     0.2,
+				MaxDelay:   19 * time.Millisecond,
+			},
+			MinConnectTimeout: 5 * time.Second,
+		}),
+		grpc.WithContextDialer(func(ctx context.Context, s string) (gonet.Conn, error) {
+			rawHost, rawPort, err := net.SplitHostPort(s)
+			if err != nil {
+				return nil, err
+			}
+			if len(rawPort) == 0 {
+				rawPort = "443"
+			}
+			port, err := net.PortFromString(rawPort)
+			if err != nil {
+				return nil, err
+			}
+			address := net.ParseAddress(rawHost)
+			return internet.DialSystem(ctx, net.TCPDestination(address, port), nil)
+		}),
+	)
+	globalDialerMap[dest] = conn
+	return conn, err
+}

+ 70 - 0
transport/internet/grpc/encoding/clientConn.go

@@ -0,0 +1,70 @@
+// +build !confonly
+
+package encoding
+
+import (
+	"bytes"
+	"io"
+	"net"
+	"time"
+)
+
+type ClientConn struct {
+	client GunService_TunClient
+	reader io.Reader
+}
+
+func (*ClientConn) LocalAddr() net.Addr {
+	return nil
+}
+
+func (*ClientConn) RemoteAddr() net.Addr {
+	return nil
+}
+
+func (*ClientConn) SetDeadline(time.Time) error {
+	return nil
+}
+
+func (*ClientConn) SetReadDeadline(time.Time) error {
+	return nil
+}
+
+func (*ClientConn) SetWriteDeadline(time.Time) error {
+	return nil
+}
+
+func (s *ClientConn) Read(b []byte) (n int, err error) {
+	if s.reader == nil {
+		h, err := s.client.Recv()
+		if err != nil {
+			return 0, newError("unable to read from gun tunnel").Base(err)
+		}
+		s.reader = bytes.NewReader(h.Data)
+	}
+	n, err = s.reader.Read(b)
+	if err == io.EOF {
+		s.reader = nil
+		return n, nil
+	}
+	return n, err
+}
+
+func (s *ClientConn) Write(b []byte) (n int, err error) {
+	err = s.client.Send(&Hunk{Data: b})
+	if err != nil {
+		return 0, newError("Unable to send data over gun").Base(err)
+	}
+	return len(b), nil
+}
+
+func (s *ClientConn) Close() error {
+	return s.client.CloseSend()
+}
+
+func NewClientConn(client GunService_TunClient) *ClientConn {
+	return &ClientConn{
+		client: client,
+		reader: nil,
+	}
+}

+ 45 - 0
transport/internet/grpc/encoding/customSeviceName.go

@@ -0,0 +1,45 @@
+// +build !confonly
+
+package encoding
+
+import (
+	"context"
+
+	"google.golang.org/grpc"
+)
+
+func ServerDesc(name string) grpc.ServiceDesc {
+	return grpc.ServiceDesc{
+		ServiceName: name,
+		HandlerType: (*GunServiceServer)(nil),
+		Methods:     []grpc.MethodDesc{},
+		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...)
+	if err != nil {
+		return nil, err
+	}
+	x := &gunServiceTunClient{stream}
+	return x, nil
+}
+
+type GunServiceClientX interface {
+	TunCustomName(ctx context.Context, name string, opts ...grpc.CallOption) (GunService_TunClient, error)
+	Tun(ctx context.Context, opts ...grpc.CallOption) (GunService_TunClient, error)
+}
+
+func RegisterGunServiceServerX(s *grpc.Server, srv GunServiceServer, name string) {
+	desc := ServerDesc(name)
+	s.RegisterService(&desc, srv)
+}

+ 3 - 0
transport/internet/grpc/encoding/encoding.go

@@ -0,0 +1,3 @@
+package encoding
+
+//go:generate go run github.com/v2fly/v2ray-core/v4/common/errors/errorgen

+ 9 - 0
transport/internet/grpc/encoding/errors.generated.go

@@ -0,0 +1,9 @@
+package encoding
+
+import "github.com/v2fly/v2ray-core/v4/common/errors"
+
+type errPathObjHolder struct{}
+
+func newError(values ...interface{}) *errors.Error {
+	return errors.New(values...).WithPathObj(errPathObjHolder{})
+}

+ 78 - 0
transport/internet/grpc/encoding/serverconn.go

@@ -0,0 +1,78 @@
+// +build !confonly
+
+package encoding
+
+import (
+	"bytes"
+	"context"
+	"io"
+	"net"
+	"time"
+)
+
+type ServerConn struct {
+	server GunService_TunServer
+	reader io.Reader
+	over   context.CancelFunc
+}
+
+func (s *ServerConn) Read(b []byte) (n int, err error) {
+	if s.reader == nil {
+		h, err := s.server.Recv()
+		if err != nil {
+			return 0, newError("unable to read from gun tunnel").Base(err)
+		}
+		s.reader = bytes.NewReader(h.Data)
+	}
+	n, err = s.reader.Read(b)
+	if err == io.EOF {
+		s.reader = nil
+		return n, nil
+	}
+	return n, err
+}
+
+func (s *ServerConn) Write(b []byte) (n int, err error) {
+	err = s.server.Send(&Hunk{Data: b})
+	if err != nil {
+		return 0, newError("Unable to send data over gun").Base(err)
+	}
+	return len(b), nil
+}
+
+func (s *ServerConn) Close() error {
+	s.over()
+	return nil
+}
+
+func (*ServerConn) LocalAddr() net.Addr {
+	return nil
+}
+
+func (*ServerConn) RemoteAddr() net.Addr {
+	newError("gun transport do not support get remote address").AtWarning().WriteToLog()
+	return &net.UnixAddr{
+		Name: "@placeholder",
+		Net:  "unix",
+	}
+}
+
+func (*ServerConn) SetDeadline(time.Time) error {
+	return nil
+}
+
+func (*ServerConn) SetReadDeadline(time.Time) error {
+	return nil
+}
+
+func (*ServerConn) SetWriteDeadline(time.Time) error {
+	return nil
+}
+
+func NewServerConn(server GunService_TunServer, over context.CancelFunc) *ServerConn {
+	return &ServerConn{
+		server: server,
+		reader: nil,
+		over:   over,
+	}
+}

+ 164 - 0
transport/internet/grpc/encoding/stream.pb.go

@@ -0,0 +1,164 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// 	protoc-gen-go v1.25.0
+// 	protoc        v3.13.0
+// source: transport/internet/grpc/encoding/stream.proto
+
+package encoding
+
+import (
+	proto "github.com/golang/protobuf/proto"
+	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+	reflect "reflect"
+	sync "sync"
+)
+
+const (
+	// Verify that this generated code is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+	// Verify that runtime/protoimpl is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+// This is a compile-time assertion that a sufficiently up-to-date version
+// of the legacy proto package is being used.
+const _ = proto.ProtoPackageIsVersion4
+
+type Hunk struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	Data []byte `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"`
+}
+
+func (x *Hunk) Reset() {
+	*x = Hunk{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_transport_internet_grpc_encoding_stream_proto_msgTypes[0]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *Hunk) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*Hunk) ProtoMessage() {}
+
+func (x *Hunk) ProtoReflect() protoreflect.Message {
+	mi := &file_transport_internet_grpc_encoding_stream_proto_msgTypes[0]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use Hunk.ProtoReflect.Descriptor instead.
+func (*Hunk) Descriptor() ([]byte, []int) {
+	return file_transport_internet_grpc_encoding_stream_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *Hunk) GetData() []byte {
+	if x != nil {
+		return x.Data
+	}
+	return nil
+}
+
+var File_transport_internet_grpc_encoding_stream_proto protoreflect.FileDescriptor
+
+var file_transport_internet_grpc_encoding_stream_proto_rawDesc = []byte{
+	0x0a, 0x2d, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x69, 0x6e, 0x74, 0x65,
+	0x72, 0x6e, 0x65, 0x74, 0x2f, 0x67, 0x72, 0x70, 0x63, 0x2f, 0x65, 0x6e, 0x63, 0x6f, 0x64, 0x69,
+	0x6e, 0x67, 0x2f, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12,
+	0x2b, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x74, 0x72, 0x61, 0x6e,
+	0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x67,
+	0x72, 0x70, 0x63, 0x2e, 0x65, 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x22, 0x1a, 0x0a, 0x04,
+	0x48, 0x75, 0x6e, 0x6b, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01,
+	0x28, 0x0c, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x32, 0x7d, 0x0a, 0x0a, 0x47, 0x75, 0x6e, 0x53,
+	0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x6f, 0x0a, 0x03, 0x54, 0x75, 0x6e, 0x12, 0x31, 0x2e,
+	0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73,
+	0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x67, 0x72,
+	0x70, 0x63, 0x2e, 0x65, 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x2e, 0x48, 0x75, 0x6e, 0x6b,
+	0x1a, 0x31, 0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x74, 0x72,
+	0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74,
+	0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x65, 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x2e, 0x48,
+	0x75, 0x6e, 0x6b, 0x28, 0x01, 0x30, 0x01, 0x42, 0x41, 0x5a, 0x3f, 0x67, 0x69, 0x74, 0x68, 0x75,
+	0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x76, 0x32, 0x66, 0x6c, 0x79, 0x2f, 0x76, 0x32, 0x72, 0x61,
+	0x79, 0x2d, 0x63, 0x6f, 0x72, 0x65, 0x2f, 0x76, 0x34, 0x2f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70,
+	0x6f, 0x72, 0x74, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2f, 0x67, 0x72, 0x70,
+	0x63, 0x2f, 0x65, 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74,
+	0x6f, 0x33,
+}
+
+var (
+	file_transport_internet_grpc_encoding_stream_proto_rawDescOnce sync.Once
+	file_transport_internet_grpc_encoding_stream_proto_rawDescData = file_transport_internet_grpc_encoding_stream_proto_rawDesc
+)
+
+func file_transport_internet_grpc_encoding_stream_proto_rawDescGZIP() []byte {
+	file_transport_internet_grpc_encoding_stream_proto_rawDescOnce.Do(func() {
+		file_transport_internet_grpc_encoding_stream_proto_rawDescData = protoimpl.X.CompressGZIP(file_transport_internet_grpc_encoding_stream_proto_rawDescData)
+	})
+	return file_transport_internet_grpc_encoding_stream_proto_rawDescData
+}
+
+var file_transport_internet_grpc_encoding_stream_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
+var file_transport_internet_grpc_encoding_stream_proto_goTypes = []interface{}{
+	(*Hunk)(nil), // 0: v2ray.core.transport.internet.grpc.encoding.Hunk
+}
+var file_transport_internet_grpc_encoding_stream_proto_depIdxs = []int32{
+	0, // 0: v2ray.core.transport.internet.grpc.encoding.GunService.Tun:input_type -> v2ray.core.transport.internet.grpc.encoding.Hunk
+	0, // 1: v2ray.core.transport.internet.grpc.encoding.GunService.Tun:output_type -> v2ray.core.transport.internet.grpc.encoding.Hunk
+	1, // [1:2] is the sub-list for method output_type
+	0, // [0:1] is the sub-list for method input_type
+	0, // [0:0] is the sub-list for extension type_name
+	0, // [0:0] is the sub-list for extension extendee
+	0, // [0:0] is the sub-list for field type_name
+}
+
+func init() { file_transport_internet_grpc_encoding_stream_proto_init() }
+func file_transport_internet_grpc_encoding_stream_proto_init() {
+	if File_transport_internet_grpc_encoding_stream_proto != nil {
+		return
+	}
+	if !protoimpl.UnsafeEnabled {
+		file_transport_internet_grpc_encoding_stream_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*Hunk); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+	}
+	type x struct{}
+	out := protoimpl.TypeBuilder{
+		File: protoimpl.DescBuilder{
+			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+			RawDescriptor: file_transport_internet_grpc_encoding_stream_proto_rawDesc,
+			NumEnums:      0,
+			NumMessages:   1,
+			NumExtensions: 0,
+			NumServices:   1,
+		},
+		GoTypes:           file_transport_internet_grpc_encoding_stream_proto_goTypes,
+		DependencyIndexes: file_transport_internet_grpc_encoding_stream_proto_depIdxs,
+		MessageInfos:      file_transport_internet_grpc_encoding_stream_proto_msgTypes,
+	}.Build()
+	File_transport_internet_grpc_encoding_stream_proto = out.File
+	file_transport_internet_grpc_encoding_stream_proto_rawDesc = nil
+	file_transport_internet_grpc_encoding_stream_proto_goTypes = nil
+	file_transport_internet_grpc_encoding_stream_proto_depIdxs = nil
+}

+ 11 - 0
transport/internet/grpc/encoding/stream.proto

@@ -0,0 +1,11 @@
+syntax = "proto3";
+package v2ray.core.transport.internet.grpc.encoding;
+option go_package = "github.com/v2fly/v2ray-core/v4/transport/internet/grpc/encoding";
+
+message Hunk {
+  bytes data = 1;
+}
+
+service GunService {
+  rpc Tun (stream Hunk) returns (stream Hunk);
+}

+ 133 - 0
transport/internet/grpc/encoding/stream_grpc.pb.go

@@ -0,0 +1,133 @@
+// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
+
+package encoding
+
+import (
+	context "context"
+	grpc "google.golang.org/grpc"
+	codes "google.golang.org/grpc/codes"
+	status "google.golang.org/grpc/status"
+)
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the grpc package it is being compiled against.
+// Requires gRPC-Go v1.32.0 or later.
+const _ = grpc.SupportPackageIsVersion7
+
+// GunServiceClient is the client API for GunService service.
+//
+// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
+type GunServiceClient interface {
+	Tun(ctx context.Context, opts ...grpc.CallOption) (GunService_TunClient, error)
+}
+
+type gunServiceClient struct {
+	cc grpc.ClientConnInterface
+}
+
+func NewGunServiceClient(cc grpc.ClientConnInterface) GunServiceClient {
+	return &gunServiceClient{cc}
+}
+
+func (c *gunServiceClient) Tun(ctx context.Context, opts ...grpc.CallOption) (GunService_TunClient, error) {
+	stream, err := c.cc.NewStream(ctx, &GunService_ServiceDesc.Streams[0], "/v2ray.core.transport.internet.grpc.encoding.GunService/Tun", opts...)
+	if err != nil {
+		return nil, err
+	}
+	x := &gunServiceTunClient{stream}
+	return x, nil
+}
+
+type GunService_TunClient interface {
+	Send(*Hunk) error
+	Recv() (*Hunk, error)
+	grpc.ClientStream
+}
+
+type gunServiceTunClient struct {
+	grpc.ClientStream
+}
+
+func (x *gunServiceTunClient) Send(m *Hunk) error {
+	return x.ClientStream.SendMsg(m)
+}
+
+func (x *gunServiceTunClient) Recv() (*Hunk, error) {
+	m := new(Hunk)
+	if err := x.ClientStream.RecvMsg(m); err != nil {
+		return nil, err
+	}
+	return m, nil
+}
+
+// GunServiceServer is the server API for GunService service.
+// All implementations must embed UnimplementedGunServiceServer
+// for forward compatibility
+type GunServiceServer interface {
+	Tun(GunService_TunServer) error
+	mustEmbedUnimplementedGunServiceServer()
+}
+
+// UnimplementedGunServiceServer must be embedded to have forward compatible implementations.
+type UnimplementedGunServiceServer struct {
+}
+
+func (UnimplementedGunServiceServer) Tun(GunService_TunServer) error {
+	return status.Errorf(codes.Unimplemented, "method Tun not implemented")
+}
+func (UnimplementedGunServiceServer) mustEmbedUnimplementedGunServiceServer() {}
+
+// UnsafeGunServiceServer may be embedded to opt out of forward compatibility for this service.
+// Use of this interface is not recommended, as added methods to GunServiceServer will
+// result in compilation errors.
+type UnsafeGunServiceServer interface {
+	mustEmbedUnimplementedGunServiceServer()
+}
+
+func RegisterGunServiceServer(s grpc.ServiceRegistrar, srv GunServiceServer) {
+	s.RegisterService(&GunService_ServiceDesc, srv)
+}
+
+func _GunService_Tun_Handler(srv interface{}, stream grpc.ServerStream) error {
+	return srv.(GunServiceServer).Tun(&gunServiceTunServer{stream})
+}
+
+type GunService_TunServer interface {
+	Send(*Hunk) error
+	Recv() (*Hunk, error)
+	grpc.ServerStream
+}
+
+type gunServiceTunServer struct {
+	grpc.ServerStream
+}
+
+func (x *gunServiceTunServer) Send(m *Hunk) error {
+	return x.ServerStream.SendMsg(m)
+}
+
+func (x *gunServiceTunServer) Recv() (*Hunk, error) {
+	m := new(Hunk)
+	if err := x.ServerStream.RecvMsg(m); err != nil {
+		return nil, err
+	}
+	return m, nil
+}
+
+// GunService_ServiceDesc is the grpc.ServiceDesc for GunService service.
+// It's only intended for direct use with grpc.RegisterService,
+// and not to be introspected or modified (even as a copy)
+var GunService_ServiceDesc = grpc.ServiceDesc{
+	ServiceName: "v2ray.core.transport.internet.grpc.encoding.GunService",
+	HandlerType: (*GunServiceServer)(nil),
+	Methods:     []grpc.MethodDesc{},
+	Streams: []grpc.StreamDesc{
+		{
+			StreamName:    "Tun",
+			Handler:       _GunService_Tun_Handler,
+			ServerStreams: true,
+			ClientStreams: true,
+		},
+	},
+	Metadata: "transport/internet/grpc/encoding/stream.proto",
+}

+ 9 - 0
transport/internet/grpc/errors.generated.go

@@ -0,0 +1,9 @@
+package grpc
+
+import "github.com/v2fly/v2ray-core/v4/common/errors"
+
+type errPathObjHolder struct{}
+
+func newError(values ...interface{}) *errors.Error {
+	return errors.New(values...).WithPathObj(errPathObjHolder{})
+}

+ 5 - 0
transport/internet/grpc/grpc.go

@@ -0,0 +1,5 @@
+// +build !confonly
+
+package grpc
+
+//go:generate go run github.com/v2fly/v2ray-core/v4/common/errors/errorgen

+ 123 - 0
transport/internet/grpc/hub.go

@@ -0,0 +1,123 @@
+// +build !confonly
+
+package grpc
+
+import (
+	"context"
+
+	"github.com/v2fly/v2ray-core/v4/common"
+	"github.com/v2fly/v2ray-core/v4/common/net"
+	"github.com/v2fly/v2ray-core/v4/common/session"
+	"github.com/v2fly/v2ray-core/v4/transport/internet"
+	"github.com/v2fly/v2ray-core/v4/transport/internet/grpc/encoding"
+	"github.com/v2fly/v2ray-core/v4/transport/internet/tls"
+	"google.golang.org/grpc"
+	"google.golang.org/grpc/credentials"
+)
+
+type Listener struct {
+	encoding.UnimplementedGunServiceServer
+	ctx     context.Context
+	handler internet.ConnHandler
+	local   net.Addr
+	config  *Config
+	locker  *internet.FileLocker // for unix domain socket
+
+	s *grpc.Server
+}
+
+func (l Listener) Tun(server encoding.GunService_TunServer) error {
+	tunCtx, cancel := context.WithCancel(l.ctx)
+	l.handler(encoding.NewServerConn(server, cancel))
+	<-tunCtx.Done()
+	return nil
+}
+
+func (l Listener) Close() error {
+	l.s.Stop()
+	return nil
+}
+
+func (l Listener) Addr() net.Addr {
+	return l.local
+}
+
+func Listen(ctx context.Context, address net.Address, port net.Port, settings *internet.MemoryStreamConfig, handler internet.ConnHandler) (internet.Listener, error) {
+	httpSettings := settings.ProtocolSettings.(*Config)
+	var listener *Listener
+	if port == net.Port(0) { // unix
+		listener = &Listener{
+			handler: handler,
+			local: &net.UnixAddr{
+				Name: address.Domain(),
+				Net:  "unix",
+			},
+			config: httpSettings,
+		}
+	} else { // tcp
+		listener = &Listener{
+			handler: handler,
+			local: &net.TCPAddr{
+				IP:   address.IP(),
+				Port: int(port),
+			},
+			config: httpSettings,
+		}
+	}
+
+	listener.ctx = ctx
+
+	config := tls.ConfigFromStreamSettings(settings)
+
+	var s *grpc.Server
+	if config == nil {
+		s = grpc.NewServer()
+	} else {
+		s = grpc.NewServer(grpc.Creds(credentials.NewTLS(config.GetTLSConfig(tls.WithNextProto("h2")))))
+	}
+	listener.s = s
+
+	if settings.SocketSettings != nil && settings.SocketSettings.AcceptProxyProtocol {
+		newError("accepting PROXY protocol").AtWarning().WriteToLog(session.ExportIDToError(ctx))
+	}
+
+	go func() {
+		var streamListener net.Listener
+		var err error
+		if port == net.Port(0) { // unix
+			streamListener, err = internet.ListenSystem(ctx, &net.UnixAddr{
+				Name: address.Domain(),
+				Net:  "unix",
+			}, settings.SocketSettings)
+			if err != nil {
+				newError("failed to listen on ", address).Base(err).AtError().WriteToLog(session.ExportIDToError(ctx))
+				return
+			}
+			locker := ctx.Value(address.Domain())
+			if locker != nil {
+				listener.locker = locker.(*internet.FileLocker)
+			}
+		} else { // tcp
+			streamListener, err = internet.ListenSystem(ctx, &net.TCPAddr{
+				IP:   address.IP(),
+				Port: int(port),
+			}, settings.SocketSettings)
+			if err != nil {
+				newError("failed to listen on ", address, ":", port).Base(err).AtError().WriteToLog(session.ExportIDToError(ctx))
+				return
+			}
+		}
+
+		encoding.RegisterGunServiceServerX(s, listener, config.ServerName)
+
+		if err = s.Serve(streamListener); err != nil {
+			newError("Listener for grpc ended").Base(err).WriteToLog()
+		}
+	}()
+
+	return listener, nil
+}
+
+func init() {
+	common.Must(internet.RegisterTransportListener(protocolName, Listen))
+}