From 75db7701b625d3d5b95198607eb4823479f096e9 Mon Sep 17 00:00:00 2001 From: DVK Date: Wed, 17 Jan 2024 07:07:56 +0800 Subject: [PATCH] feat: support grpc tunnel (#19) --- go.mod | 5 +- pkg/proto/Makefile | 10 ++ pkg/proto/tunnel.pb.go | 162 ++++++++++++++++++++++++++++ pkg/proto/tunnel.proto | 14 +++ pkg/proto/tunnel_grpc.pb.go | 205 ++++++++++++++++++++++++++++++++++++ pkg/tunnel/grpc.go | 115 ++++++++++++++++++++ s.yaml | 56 +++++++--- server/service/grpc.go | 81 ++++++++++++++ 8 files changed, 630 insertions(+), 18 deletions(-) create mode 100644 pkg/proto/Makefile create mode 100644 pkg/proto/tunnel.pb.go create mode 100644 pkg/proto/tunnel.proto create mode 100644 pkg/proto/tunnel_grpc.pb.go create mode 100644 pkg/tunnel/grpc.go create mode 100644 server/service/grpc.go diff --git a/go.mod b/go.mod index c3d7b97..69c1ea6 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,8 @@ require ( github.com/spf13/cobra v1.8.0 github.com/tg123/go-htpasswd v1.2.0 github.com/xtaci/smux v1.5.24 + google.golang.org/grpc v1.37.0 + google.golang.org/protobuf v1.30.0 ) require ( @@ -23,6 +25,7 @@ require ( github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.14.0 // indirect github.com/goccy/go-json v0.10.2 // indirect + github.com/golang/protobuf v1.5.2 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/google/go-cmp v0.5.9 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect @@ -41,6 +44,6 @@ require ( golang.org/x/net v0.17.0 // indirect golang.org/x/sys v0.13.0 // indirect golang.org/x/text v0.13.0 // indirect - google.golang.org/protobuf v1.30.0 // indirect + google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/pkg/proto/Makefile b/pkg/proto/Makefile new file mode 100644 index 0000000..9c522e7 --- /dev/null +++ b/pkg/proto/Makefile @@ -0,0 +1,10 @@ +.PHONY: all +all: generate + + +generate: + protoc --proto_path=./ --go_out=./ \ + --go_opt=Mtunnel.proto=./ \ + --go-grpc_out=./ \ + --go-grpc_opt=Mtunnel.proto=./ \ + ./*.proto diff --git a/pkg/proto/tunnel.pb.go b/pkg/proto/tunnel.pb.go new file mode 100644 index 0000000..8486c0f --- /dev/null +++ b/pkg/proto/tunnel.pb.go @@ -0,0 +1,162 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.28.0 +// protoc v3.3.0 +// source: tunnel.proto + +package proto + +import ( + 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) +) + +type Chunk struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Body []byte `protobuf:"bytes,1,opt,name=body,proto3" json:"body,omitempty"` + Size int32 `protobuf:"varint,2,opt,name=size,proto3" json:"size,omitempty"` +} + +func (x *Chunk) Reset() { + *x = Chunk{} + if protoimpl.UnsafeEnabled { + mi := &file_tunnel_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Chunk) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Chunk) ProtoMessage() {} + +func (x *Chunk) ProtoReflect() protoreflect.Message { + mi := &file_tunnel_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 Chunk.ProtoReflect.Descriptor instead. +func (*Chunk) Descriptor() ([]byte, []int) { + return file_tunnel_proto_rawDescGZIP(), []int{0} +} + +func (x *Chunk) GetBody() []byte { + if x != nil { + return x.Body + } + return nil +} + +func (x *Chunk) GetSize() int32 { + if x != nil { + return x.Size + } + return 0 +} + +var File_tunnel_proto protoreflect.FileDescriptor + +var file_tunnel_proto_rawDesc = []byte{ + 0x0a, 0x0c, 0x74, 0x75, 0x6e, 0x6e, 0x65, 0x6c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x06, + 0x74, 0x75, 0x6e, 0x6e, 0x65, 0x6c, 0x22, 0x2f, 0x0a, 0x05, 0x43, 0x68, 0x75, 0x6e, 0x6b, 0x12, + 0x12, 0x0a, 0x04, 0x62, 0x6f, 0x64, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x62, + 0x6f, 0x64, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x05, 0x52, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x32, 0x5e, 0x0a, 0x06, 0x54, 0x75, 0x6e, 0x6e, 0x65, + 0x6c, 0x12, 0x28, 0x0a, 0x04, 0x48, 0x74, 0x74, 0x70, 0x12, 0x0d, 0x2e, 0x74, 0x75, 0x6e, 0x6e, + 0x65, 0x6c, 0x2e, 0x43, 0x68, 0x75, 0x6e, 0x6b, 0x1a, 0x0d, 0x2e, 0x74, 0x75, 0x6e, 0x6e, 0x65, + 0x6c, 0x2e, 0x43, 0x68, 0x75, 0x6e, 0x6b, 0x28, 0x01, 0x30, 0x01, 0x12, 0x2a, 0x0a, 0x06, 0x53, + 0x6f, 0x63, 0x6b, 0x73, 0x35, 0x12, 0x0d, 0x2e, 0x74, 0x75, 0x6e, 0x6e, 0x65, 0x6c, 0x2e, 0x43, + 0x68, 0x75, 0x6e, 0x6b, 0x1a, 0x0d, 0x2e, 0x74, 0x75, 0x6e, 0x6e, 0x65, 0x6c, 0x2e, 0x43, 0x68, + 0x75, 0x6e, 0x6b, 0x28, 0x01, 0x30, 0x01, 0x42, 0x23, 0x5a, 0x21, 0x67, 0x69, 0x74, 0x68, 0x75, + 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x64, 0x76, 0x6b, 0x75, 0x6e, 0x69, 0x6f, 0x6e, 0x2f, 0x73, + 0x65, 0x61, 0x6d, 0x6f, 0x6f, 0x6e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_tunnel_proto_rawDescOnce sync.Once + file_tunnel_proto_rawDescData = file_tunnel_proto_rawDesc +) + +func file_tunnel_proto_rawDescGZIP() []byte { + file_tunnel_proto_rawDescOnce.Do(func() { + file_tunnel_proto_rawDescData = protoimpl.X.CompressGZIP(file_tunnel_proto_rawDescData) + }) + return file_tunnel_proto_rawDescData +} + +var file_tunnel_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_tunnel_proto_goTypes = []interface{}{ + (*Chunk)(nil), // 0: tunnel.Chunk +} +var file_tunnel_proto_depIdxs = []int32{ + 0, // 0: tunnel.Tunnel.Http:input_type -> tunnel.Chunk + 0, // 1: tunnel.Tunnel.Socks5:input_type -> tunnel.Chunk + 0, // 2: tunnel.Tunnel.Http:output_type -> tunnel.Chunk + 0, // 3: tunnel.Tunnel.Socks5:output_type -> tunnel.Chunk + 2, // [2:4] is the sub-list for method output_type + 0, // [0:2] 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_tunnel_proto_init() } +func file_tunnel_proto_init() { + if File_tunnel_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_tunnel_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Chunk); 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_tunnel_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_tunnel_proto_goTypes, + DependencyIndexes: file_tunnel_proto_depIdxs, + MessageInfos: file_tunnel_proto_msgTypes, + }.Build() + File_tunnel_proto = out.File + file_tunnel_proto_rawDesc = nil + file_tunnel_proto_goTypes = nil + file_tunnel_proto_depIdxs = nil +} diff --git a/pkg/proto/tunnel.proto b/pkg/proto/tunnel.proto new file mode 100644 index 0000000..ebc360d --- /dev/null +++ b/pkg/proto/tunnel.proto @@ -0,0 +1,14 @@ +syntax = "proto3"; + +package tunnel; +option go_package = "github.com/dvkunion/seamoon/proto"; + +message Chunk { + bytes body = 1; + int32 size = 2; +} + +service Tunnel { + rpc Http (stream Chunk) returns (stream Chunk); + rpc Socks5 (stream Chunk) returns (stream Chunk); +} \ No newline at end of file diff --git a/pkg/proto/tunnel_grpc.pb.go b/pkg/proto/tunnel_grpc.pb.go new file mode 100644 index 0000000..cea1443 --- /dev/null +++ b/pkg/proto/tunnel_grpc.pb.go @@ -0,0 +1,205 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.2.0 +// - protoc v3.3.0 +// source: tunnel.proto + +package proto + +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 + +// TunnelClient is the client API for Tunnel 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 TunnelClient interface { + Http(ctx context.Context, opts ...grpc.CallOption) (Tunnel_HttpClient, error) + Socks5(ctx context.Context, opts ...grpc.CallOption) (Tunnel_Socks5Client, error) +} + +type tunnelClient struct { + cc grpc.ClientConnInterface +} + +func NewTunnelClient(cc grpc.ClientConnInterface) TunnelClient { + return &tunnelClient{cc} +} + +func (c *tunnelClient) Http(ctx context.Context, opts ...grpc.CallOption) (Tunnel_HttpClient, error) { + stream, err := c.cc.NewStream(ctx, &Tunnel_ServiceDesc.Streams[0], "/tunnel.Tunnel/Http", opts...) + if err != nil { + return nil, err + } + x := &tunnelHttpClient{stream} + return x, nil +} + +type Tunnel_HttpClient interface { + Send(*Chunk) error + Recv() (*Chunk, error) + grpc.ClientStream +} + +type tunnelHttpClient struct { + grpc.ClientStream +} + +func (x *tunnelHttpClient) Send(m *Chunk) error { + return x.ClientStream.SendMsg(m) +} + +func (x *tunnelHttpClient) Recv() (*Chunk, error) { + m := new(Chunk) + if err := x.ClientStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + +func (c *tunnelClient) Socks5(ctx context.Context, opts ...grpc.CallOption) (Tunnel_Socks5Client, error) { + stream, err := c.cc.NewStream(ctx, &Tunnel_ServiceDesc.Streams[1], "/tunnel.Tunnel/Socks5", opts...) + if err != nil { + return nil, err + } + x := &tunnelSocks5Client{stream} + return x, nil +} + +type Tunnel_Socks5Client interface { + Send(*Chunk) error + Recv() (*Chunk, error) + grpc.ClientStream +} + +type tunnelSocks5Client struct { + grpc.ClientStream +} + +func (x *tunnelSocks5Client) Send(m *Chunk) error { + return x.ClientStream.SendMsg(m) +} + +func (x *tunnelSocks5Client) Recv() (*Chunk, error) { + m := new(Chunk) + if err := x.ClientStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + +// TunnelServer is the server API for Tunnel service. +// All implementations must embed UnimplementedTunnelServer +// for forward compatibility +type TunnelServer interface { + Http(Tunnel_HttpServer) error + Socks5(Tunnel_Socks5Server) error + mustEmbedUnimplementedTunnelServer() +} + +// UnimplementedTunnelServer must be embedded to have forward compatible implementations. +type UnimplementedTunnelServer struct { +} + +func (UnimplementedTunnelServer) Http(Tunnel_HttpServer) error { + return status.Errorf(codes.Unimplemented, "method Http not implemented") +} +func (UnimplementedTunnelServer) Socks5(Tunnel_Socks5Server) error { + return status.Errorf(codes.Unimplemented, "method Socks5 not implemented") +} +func (UnimplementedTunnelServer) mustEmbedUnimplementedTunnelServer() {} + +// UnsafeTunnelServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to TunnelServer will +// result in compilation errors. +type UnsafeTunnelServer interface { + mustEmbedUnimplementedTunnelServer() +} + +func RegisterTunnelServer(s grpc.ServiceRegistrar, srv TunnelServer) { + s.RegisterService(&Tunnel_ServiceDesc, srv) +} + +func _Tunnel_Http_Handler(srv interface{}, stream grpc.ServerStream) error { + return srv.(TunnelServer).Http(&tunnelHttpServer{stream}) +} + +type Tunnel_HttpServer interface { + Send(*Chunk) error + Recv() (*Chunk, error) + grpc.ServerStream +} + +type tunnelHttpServer struct { + grpc.ServerStream +} + +func (x *tunnelHttpServer) Send(m *Chunk) error { + return x.ServerStream.SendMsg(m) +} + +func (x *tunnelHttpServer) Recv() (*Chunk, error) { + m := new(Chunk) + if err := x.ServerStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + +func _Tunnel_Socks5_Handler(srv interface{}, stream grpc.ServerStream) error { + return srv.(TunnelServer).Socks5(&tunnelSocks5Server{stream}) +} + +type Tunnel_Socks5Server interface { + Send(*Chunk) error + Recv() (*Chunk, error) + grpc.ServerStream +} + +type tunnelSocks5Server struct { + grpc.ServerStream +} + +func (x *tunnelSocks5Server) Send(m *Chunk) error { + return x.ServerStream.SendMsg(m) +} + +func (x *tunnelSocks5Server) Recv() (*Chunk, error) { + m := new(Chunk) + if err := x.ServerStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + +// Tunnel_ServiceDesc is the grpc.ServiceDesc for Tunnel service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var Tunnel_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "tunnel.Tunnel", + HandlerType: (*TunnelServer)(nil), + Methods: []grpc.MethodDesc{}, + Streams: []grpc.StreamDesc{ + { + StreamName: "Http", + Handler: _Tunnel_Http_Handler, + ServerStreams: true, + ClientStreams: true, + }, + { + StreamName: "Socks5", + Handler: _Tunnel_Socks5_Handler, + ServerStreams: true, + ClientStreams: true, + }, + }, + Metadata: "tunnel.proto", +} diff --git a/pkg/tunnel/grpc.go b/pkg/tunnel/grpc.go new file mode 100644 index 0000000..df4dc95 --- /dev/null +++ b/pkg/tunnel/grpc.go @@ -0,0 +1,115 @@ +package tunnel + +import ( + "context" + "errors" + "fmt" + "net" + "time" + + "google.golang.org/grpc" + + pb "github.com/DVKunion/SeaMoon/pkg/proto" +) + +type grpcConn struct { + cc grpc.Stream + + rb []byte + lAddr net.Addr + rAddr net.Addr +} + +func GRPCWrapConn(addr net.Addr, cc grpc.Stream) Tunnel { + return &grpcConn{ + cc: cc, + lAddr: addr, + rAddr: &net.TCPAddr{}, + } +} + +func (c *grpcConn) Read(b []byte) (n int, err error) { + if len(c.rb) == 0 { + chunk, err := c.recv() + if err != nil { + return 0, err + } + c.rb = chunk.Body + } + + n = copy(b, c.rb) + c.rb = c.rb[n:] + return +} + +func (c *grpcConn) Write(b []byte) (n int, err error) { + chunk := &pb.Chunk{ + Body: b, + Size: int32(len(b)), + } + + if err = c.send(chunk); err != nil { + return + } + + n = int(chunk.Size) + return +} + +func (c *grpcConn) Close() error { + switch cost := c.cc.(type) { + case pb.Tunnel_HttpClient: + case pb.Tunnel_Socks5Client: + return cost.CloseSend() + } + return nil +} + +func (c *grpcConn) LocalAddr() net.Addr { + return c.lAddr +} + +func (c *grpcConn) RemoteAddr() net.Addr { + return c.rAddr +} + +func (c *grpcConn) SetDeadline(t time.Time) error { + return &net.OpError{Op: "set", Net: "grpc", Source: nil, Addr: nil, Err: errors.New("deadline not supported")} +} + +func (c *grpcConn) SetReadDeadline(t time.Time) error { + return &net.OpError{Op: "set", Net: "grpc", Source: nil, Addr: nil, Err: errors.New("deadline not supported")} +} + +func (c *grpcConn) SetWriteDeadline(t time.Time) error { + return &net.OpError{Op: "set", Net: "grpc", Source: nil, Addr: nil, Err: errors.New("deadline not supported")} +} + +func (c *grpcConn) context() context.Context { + if c.cc != nil { + return c.cc.Context() + } + return context.Background() +} + +func (c *grpcConn) send(data *pb.Chunk) error { + sender, ok := c.cc.(interface { + Send(*pb.Chunk) error + }) + if !ok { + // todo + return fmt.Errorf("unsupported type: %T", c.cc) + } + return sender.Send(data) +} + +func (c *grpcConn) recv() (*pb.Chunk, error) { + receiver, ok := c.cc.(interface { + Recv() (*pb.Chunk, error) + }) + if !ok { + // todo + return nil, fmt.Errorf("unsupported type: %T", c.cc) + } + return receiver.Recv() +} diff --git a/s.yaml b/s.yaml index b07c808..9c772d7 100644 --- a/s.yaml +++ b/s.yaml @@ -22,14 +22,14 @@ actions: path: ./ services: - SeaMoon-WS-Node: + SeaMoon-WST-Node: component: fc props: region: ${vars.region} service: ${vars.service} function: - name: ws-node # 请不要修改函数名,因为配置处需要使用磁名称来判断函数类型。 - description: 'http-proxy' + name: ws-node + description: 'websocket-proxy-server' codeUri: './' customRuntimeConfig: command: @@ -53,17 +53,39 @@ services: methods: - GET - POST - - PUT - - DELETE - - OPTIONS - customDomains: - - domainName: auto - protocol: HTTP - routeConfigs: - - path: /* - methods: - - GET - - POST - - PUT - - DELETE - - OPTIONS \ No newline at end of file + SeaMoon-GRT-Node: + component: fc + props: + region: ${vars.region} + service: ${vars.service} + function: + name: grpc-node + description: 'grpc-proxy-server' + codeUri: './' + caPort: 8089 + customRuntimeConfig: + command: + - ./seamoon + args: + - "server" + - "-p" + - "8089" + - "-t" + - "grpc" + handler: main + instanceConcurrency: 10 + instanceType: e1 + cpu: 0.05 + diskSize: 512 + memorySize: 128 + runtime: custom + timeout: 300 + internetAccess: true + triggers: + - name: httpTrigger + type: http + config: + authType: anonymous + methods: + - GET + - POST \ No newline at end of file diff --git a/server/service/grpc.go b/server/service/grpc.go new file mode 100644 index 0000000..eb0c78b --- /dev/null +++ b/server/service/grpc.go @@ -0,0 +1,81 @@ +package service + +import ( + "log/slog" + "net" + "time" + + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/keepalive" + + pb "github.com/DVKunion/SeaMoon/pkg/proto" + "github.com/DVKunion/SeaMoon/pkg/transfer" + "github.com/DVKunion/SeaMoon/pkg/tunnel" +) + +type GRPCService struct { + addr net.Addr + server *grpc.Server + + pb.UnimplementedTunnelServer +} + +func init() { + register(tunnel.GRT, &GRPCService{}) +} + +func (g GRPCService) Serve(ln net.Listener, srvOpt ...Option) error { + var srvOpts = &Options{} + for _, o := range srvOpt { + o(srvOpts) + } + var gRPCOpts []grpc.ServerOption + if srvOpts.tlsConf != nil { + gRPCOpts = append(gRPCOpts, grpc.Creds(credentials.NewTLS(srvOpts.tlsConf))) + } + + if srvOpts.keepalive != nil { + gRPCOpts = append(gRPCOpts, + grpc.KeepaliveParams(keepalive.ServerParameters{ + Time: 10 * time.Second, // send pings every 10 seconds if there is no activity + Timeout: 3 * time.Second, // wait 1 second for ping ack before considering the connection dead + MaxConnectionIdle: 30 * time.Second, + //MaxConnectionIdle: srvOpts.keepalive.MaxConnectionIdle, + //Time: srvOpts.keepalive.MaxTime, + //Timeout: srvOpts.keepalive.Timeout, + }), + grpc.KeepaliveEnforcementPolicy(keepalive.EnforcementPolicy{ + //MinTime: srvOpts.keepalive.MinTime, + PermitWithoutStream: false, + }), + ) + } + + server := grpc.NewServer(gRPCOpts...) + + pb.RegisterTunnelServer(server, &g) + + return server.Serve(ln) +} + +func (g GRPCService) Http(server pb.Tunnel_HttpServer) error { + gt := tunnel.GRPCWrapConn(g.addr, server) + + if err := transfer.HttpTransport(gt); err != nil { + slog.Error("connection error", "msg", err) + return err + } + + return nil +} + +func (g GRPCService) Socks5(server pb.Tunnel_Socks5Server) error { + gt := tunnel.GRPCWrapConn(g.addr, server) + + if err := transfer.Socks5Transport(gt); err != nil { + slog.Error("connection error", "msg", err) + return err + } + return nil +}