diff --git a/cmd/lima-guestagent/daemon_linux.go b/cmd/lima-guestagent/daemon_linux.go index e03fa00ae7eb..391ae6672ea8 100644 --- a/cmd/lima-guestagent/daemon_linux.go +++ b/cmd/lima-guestagent/daemon_linux.go @@ -9,6 +9,7 @@ import ( "github.com/lima-vm/lima/pkg/guestagent" "github.com/lima-vm/lima/pkg/guestagent/api/server" "github.com/lima-vm/lima/pkg/guestagent/serialport" + "github.com/lima-vm/lima/pkg/portfwdserver" "github.com/mdlayher/vsock" "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -91,5 +92,5 @@ func daemonAction(cmd *cobra.Command, _ []string) error { l = socketL logrus.Infof("serving the guest agent on %q", socket) } - return server.StartServer(l, &server.GuestServer{Agent: agent}) + return server.StartServer(l, &server.GuestServer{Agent: agent, TunnelS: portfwdserver.NewTunnelServer()}) } diff --git a/pkg/guestagent/api/client/client.go b/pkg/guestagent/api/client/client.go index 998788bc0705..d4bdf27e6ab7 100644 --- a/pkg/guestagent/api/client/client.go +++ b/pkg/guestagent/api/client/client.go @@ -2,6 +2,7 @@ package client import ( "context" + "math" "net" "github.com/lima-vm/lima/pkg/guestagent/api" @@ -16,6 +17,10 @@ type GuestAgentClient struct { func NewGuestAgentClient(dialFn func(ctx context.Context) (net.Conn, error)) (*GuestAgentClient, error) { opts := []grpc.DialOption{ + grpc.WithDefaultCallOptions( + grpc.MaxCallRecvMsgSize(math.MaxInt64), + grpc.MaxCallSendMsgSize(math.MaxInt64), + ), grpc.WithContextDialer(func(ctx context.Context, _ string) (net.Conn, error) { return dialFn(ctx) }), @@ -59,3 +64,11 @@ func (c *GuestAgentClient) Inotify(ctx context.Context) (api.GuestService_PostIn } return inotify, nil } + +func (c *GuestAgentClient) Tunnel(ctx context.Context) (api.GuestService_TunnelClient, error) { + stream, err := c.cli.Tunnel(ctx) + if err != nil { + return nil, err + } + return stream, nil +} diff --git a/pkg/guestagent/api/guestservice.pb.desc b/pkg/guestagent/api/guestservice.pb.desc index 415d90c53b33..2155145e47dd 100644 --- a/pkg/guestagent/api/guestservice.pb.desc +++ b/pkg/guestagent/api/guestservice.pb.desc @@ -1,5 +1,5 @@ -� +� guestservice.protogoogle/protobuf/empty.protogoogle/protobuf/timestamp.proto"0 Info( local_ports (2.IPPortR @@ -8,15 +8,23 @@ localPorts" time (2.google.protobuf.TimestampRtime3 local_ports_added (2.IPPortRlocalPortsAdded7 local_ports_removed (2.IPPortRlocalPortsRemoved -errors ( Rerrors", -IPPort -ip ( Rip -port (Rport"X +errors ( Rerrors"H +IPPort +protocol ( Rprotocol +ip ( Rip +port (Rport"X Inotify mount_path ( R mountPath. -time (2.google.protobuf.TimestampRtime2� +time (2.google.protobuf.TimestampRtime"� + TunnelMessage +id ( Rid +protocol ( Rprotocol +data (Rdata + guestAddr ( R guestAddr$ + udpTargetAddr ( R udpTargetAddr2� GuestService( GetInfo.google.protobuf.Empty.Info- GetEvents.google.protobuf.Empty.Event01 -PostInotify.Inotify.google.protobuf.Empty(B!Zgithub.com/lima-vm/lima/pkg/apibproto3 \ No newline at end of file +PostInotify.Inotify.google.protobuf.Empty(, +Tunnel.TunnelMessage.TunnelMessage(0B!Zgithub.com/lima-vm/lima/pkg/apibproto3 \ No newline at end of file diff --git a/pkg/guestagent/api/guestservice.pb.go b/pkg/guestagent/api/guestservice.pb.go index cfad0613dce5..27bb5983deee 100644 --- a/pkg/guestagent/api/guestservice.pb.go +++ b/pkg/guestagent/api/guestservice.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.28.1 -// protoc v4.25.3 +// protoc v5.27.1 // source: guestservice.proto package api @@ -145,8 +145,9 @@ type IPPort struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Ip string `protobuf:"bytes,1,opt,name=ip,proto3" json:"ip,omitempty"` - Port int32 `protobuf:"varint,2,opt,name=port,proto3" json:"port,omitempty"` + Protocol string `protobuf:"bytes,1,opt,name=protocol,proto3" json:"protocol,omitempty"` //tcp, udp + Ip string `protobuf:"bytes,2,opt,name=ip,proto3" json:"ip,omitempty"` + Port int32 `protobuf:"varint,3,opt,name=port,proto3" json:"port,omitempty"` } func (x *IPPort) Reset() { @@ -181,6 +182,13 @@ func (*IPPort) Descriptor() ([]byte, []int) { return file_guestservice_proto_rawDescGZIP(), []int{2} } +func (x *IPPort) GetProtocol() string { + if x != nil { + return x.Protocol + } + return "" +} + func (x *IPPort) GetIp() string { if x != nil { return x.Ip @@ -250,6 +258,85 @@ func (x *Inotify) GetTime() *timestamppb.Timestamp { return nil } +type TunnelMessage struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Protocol string `protobuf:"bytes,2,opt,name=protocol,proto3" json:"protocol,omitempty"` //tcp, udp + Data []byte `protobuf:"bytes,3,opt,name=data,proto3" json:"data,omitempty"` + GuestAddr string `protobuf:"bytes,4,opt,name=guestAddr,proto3" json:"guestAddr,omitempty"` + UdpTargetAddr string `protobuf:"bytes,5,opt,name=udpTargetAddr,proto3" json:"udpTargetAddr,omitempty"` +} + +func (x *TunnelMessage) Reset() { + *x = TunnelMessage{} + if protoimpl.UnsafeEnabled { + mi := &file_guestservice_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *TunnelMessage) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TunnelMessage) ProtoMessage() {} + +func (x *TunnelMessage) ProtoReflect() protoreflect.Message { + mi := &file_guestservice_proto_msgTypes[4] + 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 TunnelMessage.ProtoReflect.Descriptor instead. +func (*TunnelMessage) Descriptor() ([]byte, []int) { + return file_guestservice_proto_rawDescGZIP(), []int{4} +} + +func (x *TunnelMessage) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *TunnelMessage) GetProtocol() string { + if x != nil { + return x.Protocol + } + return "" +} + +func (x *TunnelMessage) GetData() []byte { + if x != nil { + return x.Data + } + return nil +} + +func (x *TunnelMessage) GetGuestAddr() string { + if x != nil { + return x.GuestAddr + } + return "" +} + +func (x *TunnelMessage) GetUdpTargetAddr() string { + if x != nil { + return x.UdpTargetAddr + } + return "" +} + var File_guestservice_proto protoreflect.FileDescriptor var file_guestservice_proto_rawDesc = []byte{ @@ -273,25 +360,39 @@ var file_guestservice_proto_rawDesc = []byte{ 0x32, 0x07, 0x2e, 0x49, 0x50, 0x50, 0x6f, 0x72, 0x74, 0x52, 0x11, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x6f, 0x72, 0x74, 0x73, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x65, 0x72, - 0x72, 0x6f, 0x72, 0x73, 0x22, 0x2c, 0x0a, 0x06, 0x49, 0x50, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x0e, - 0x0a, 0x02, 0x69, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x70, 0x12, 0x12, - 0x0a, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x70, 0x6f, - 0x72, 0x74, 0x22, 0x58, 0x0a, 0x07, 0x49, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x79, 0x12, 0x1d, 0x0a, - 0x0a, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x09, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x50, 0x61, 0x74, 0x68, 0x12, 0x2e, 0x0a, 0x04, - 0x74, 0x69, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, - 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, - 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x04, 0x74, 0x69, 0x6d, 0x65, 0x32, 0x9a, 0x01, 0x0a, - 0x0c, 0x47, 0x75, 0x65, 0x73, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x28, 0x0a, - 0x07, 0x47, 0x65, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, - 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, - 0x1a, 0x05, 0x2e, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x2d, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x45, 0x76, - 0x65, 0x6e, 0x74, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x06, 0x2e, 0x45, - 0x76, 0x65, 0x6e, 0x74, 0x30, 0x01, 0x12, 0x31, 0x0a, 0x0b, 0x50, 0x6f, 0x73, 0x74, 0x49, 0x6e, - 0x6f, 0x74, 0x69, 0x66, 0x79, 0x12, 0x08, 0x2e, 0x49, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x79, 0x1a, - 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, - 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x28, 0x01, 0x42, 0x21, 0x5a, 0x1f, 0x67, 0x69, 0x74, + 0x72, 0x6f, 0x72, 0x73, 0x22, 0x48, 0x0a, 0x06, 0x49, 0x50, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x1a, + 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x70, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x70, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x6f, + 0x72, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x22, 0x58, + 0x0a, 0x07, 0x49, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x79, 0x12, 0x1d, 0x0a, 0x0a, 0x6d, 0x6f, 0x75, + 0x6e, 0x74, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6d, + 0x6f, 0x75, 0x6e, 0x74, 0x50, 0x61, 0x74, 0x68, 0x12, 0x2e, 0x0a, 0x04, 0x74, 0x69, 0x6d, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, + 0x6d, 0x70, 0x52, 0x04, 0x74, 0x69, 0x6d, 0x65, 0x22, 0x93, 0x01, 0x0a, 0x0d, 0x54, 0x75, 0x6e, + 0x6e, 0x65, 0x6c, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x12, 0x1c, 0x0a, 0x09, 0x67, 0x75, + 0x65, 0x73, 0x74, 0x41, 0x64, 0x64, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x67, + 0x75, 0x65, 0x73, 0x74, 0x41, 0x64, 0x64, 0x72, 0x12, 0x24, 0x0a, 0x0d, 0x75, 0x64, 0x70, 0x54, + 0x61, 0x72, 0x67, 0x65, 0x74, 0x41, 0x64, 0x64, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0d, 0x75, 0x64, 0x70, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x41, 0x64, 0x64, 0x72, 0x32, 0xc8, + 0x01, 0x0a, 0x0c, 0x47, 0x75, 0x65, 0x73, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, + 0x28, 0x0a, 0x07, 0x47, 0x65, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, + 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, + 0x74, 0x79, 0x1a, 0x05, 0x2e, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x2d, 0x0a, 0x09, 0x47, 0x65, 0x74, + 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x06, + 0x2e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x30, 0x01, 0x12, 0x31, 0x0a, 0x0b, 0x50, 0x6f, 0x73, 0x74, + 0x49, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x79, 0x12, 0x08, 0x2e, 0x49, 0x6e, 0x6f, 0x74, 0x69, 0x66, + 0x79, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x28, 0x01, 0x12, 0x2c, 0x0a, 0x06, 0x54, + 0x75, 0x6e, 0x6e, 0x65, 0x6c, 0x12, 0x0e, 0x2e, 0x54, 0x75, 0x6e, 0x6e, 0x65, 0x6c, 0x4d, 0x65, + 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x0e, 0x2e, 0x54, 0x75, 0x6e, 0x6e, 0x65, 0x6c, 0x4d, 0x65, + 0x73, 0x73, 0x61, 0x67, 0x65, 0x28, 0x01, 0x30, 0x01, 0x42, 0x21, 0x5a, 0x1f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6c, 0x69, 0x6d, 0x61, 0x2d, 0x76, 0x6d, 0x2f, 0x6c, 0x69, 0x6d, 0x61, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x61, 0x70, 0x69, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, @@ -309,29 +410,32 @@ func file_guestservice_proto_rawDescGZIP() []byte { return file_guestservice_proto_rawDescData } -var file_guestservice_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_guestservice_proto_msgTypes = make([]protoimpl.MessageInfo, 5) var file_guestservice_proto_goTypes = []interface{}{ (*Info)(nil), // 0: Info (*Event)(nil), // 1: Event (*IPPort)(nil), // 2: IPPort (*Inotify)(nil), // 3: Inotify - (*timestamppb.Timestamp)(nil), // 4: google.protobuf.Timestamp - (*emptypb.Empty)(nil), // 5: google.protobuf.Empty + (*TunnelMessage)(nil), // 4: TunnelMessage + (*timestamppb.Timestamp)(nil), // 5: google.protobuf.Timestamp + (*emptypb.Empty)(nil), // 6: google.protobuf.Empty } var file_guestservice_proto_depIdxs = []int32{ 2, // 0: Info.local_ports:type_name -> IPPort - 4, // 1: Event.time:type_name -> google.protobuf.Timestamp + 5, // 1: Event.time:type_name -> google.protobuf.Timestamp 2, // 2: Event.local_ports_added:type_name -> IPPort 2, // 3: Event.local_ports_removed:type_name -> IPPort - 4, // 4: Inotify.time:type_name -> google.protobuf.Timestamp - 5, // 5: GuestService.GetInfo:input_type -> google.protobuf.Empty - 5, // 6: GuestService.GetEvents:input_type -> google.protobuf.Empty + 5, // 4: Inotify.time:type_name -> google.protobuf.Timestamp + 6, // 5: GuestService.GetInfo:input_type -> google.protobuf.Empty + 6, // 6: GuestService.GetEvents:input_type -> google.protobuf.Empty 3, // 7: GuestService.PostInotify:input_type -> Inotify - 0, // 8: GuestService.GetInfo:output_type -> Info - 1, // 9: GuestService.GetEvents:output_type -> Event - 5, // 10: GuestService.PostInotify:output_type -> google.protobuf.Empty - 8, // [8:11] is the sub-list for method output_type - 5, // [5:8] is the sub-list for method input_type + 4, // 8: GuestService.Tunnel:input_type -> TunnelMessage + 0, // 9: GuestService.GetInfo:output_type -> Info + 1, // 10: GuestService.GetEvents:output_type -> Event + 6, // 11: GuestService.PostInotify:output_type -> google.protobuf.Empty + 4, // 12: GuestService.Tunnel:output_type -> TunnelMessage + 9, // [9:13] is the sub-list for method output_type + 5, // [5:9] is the sub-list for method input_type 5, // [5:5] is the sub-list for extension type_name 5, // [5:5] is the sub-list for extension extendee 0, // [0:5] is the sub-list for field type_name @@ -391,6 +495,18 @@ func file_guestservice_proto_init() { return nil } } + file_guestservice_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*TunnelMessage); 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{ @@ -398,7 +514,7 @@ func file_guestservice_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_guestservice_proto_rawDesc, NumEnums: 0, - NumMessages: 4, + NumMessages: 5, NumExtensions: 0, NumServices: 1, }, diff --git a/pkg/guestagent/api/guestservice.proto b/pkg/guestagent/api/guestservice.proto index 3780cbdefbcc..377cf053ef7c 100644 --- a/pkg/guestagent/api/guestservice.proto +++ b/pkg/guestagent/api/guestservice.proto @@ -8,6 +8,8 @@ service GuestService { rpc GetInfo(google.protobuf.Empty) returns (Info); rpc GetEvents(google.protobuf.Empty) returns (stream Event); rpc PostInotify(stream Inotify) returns (google.protobuf.Empty); + + rpc Tunnel(stream TunnelMessage) returns (stream TunnelMessage); } message Info { @@ -22,11 +24,20 @@ message Event { } message IPPort { - string ip = 1; - int32 port = 2; + string protocol = 1; //tcp, udp + string ip = 2; + int32 port = 3; } message Inotify { string mount_path = 1; google.protobuf.Timestamp time = 2; } + +message TunnelMessage { + string id = 1; + string protocol = 2; //tcp, udp + bytes data = 3; + string guestAddr = 4; + string udpTargetAddr = 5; +} diff --git a/pkg/guestagent/api/guestservice_grpc.pb.go b/pkg/guestagent/api/guestservice_grpc.pb.go index 475e10e1de6c..9b9fd1f6baa0 100644 --- a/pkg/guestagent/api/guestservice_grpc.pb.go +++ b/pkg/guestagent/api/guestservice_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.2.0 -// - protoc v4.25.3 +// - protoc v5.27.1 // source: guestservice.proto package api @@ -26,6 +26,7 @@ type GuestServiceClient interface { GetInfo(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*Info, error) GetEvents(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (GuestService_GetEventsClient, error) PostInotify(ctx context.Context, opts ...grpc.CallOption) (GuestService_PostInotifyClient, error) + Tunnel(ctx context.Context, opts ...grpc.CallOption) (GuestService_TunnelClient, error) } type guestServiceClient struct { @@ -111,6 +112,37 @@ func (x *guestServicePostInotifyClient) CloseAndRecv() (*emptypb.Empty, error) { return m, nil } +func (c *guestServiceClient) Tunnel(ctx context.Context, opts ...grpc.CallOption) (GuestService_TunnelClient, error) { + stream, err := c.cc.NewStream(ctx, &GuestService_ServiceDesc.Streams[2], "/GuestService/Tunnel", opts...) + if err != nil { + return nil, err + } + x := &guestServiceTunnelClient{stream} + return x, nil +} + +type GuestService_TunnelClient interface { + Send(*TunnelMessage) error + Recv() (*TunnelMessage, error) + grpc.ClientStream +} + +type guestServiceTunnelClient struct { + grpc.ClientStream +} + +func (x *guestServiceTunnelClient) Send(m *TunnelMessage) error { + return x.ClientStream.SendMsg(m) +} + +func (x *guestServiceTunnelClient) Recv() (*TunnelMessage, error) { + m := new(TunnelMessage) + if err := x.ClientStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + // GuestServiceServer is the server API for GuestService service. // All implementations must embed UnimplementedGuestServiceServer // for forward compatibility @@ -118,6 +150,7 @@ type GuestServiceServer interface { GetInfo(context.Context, *emptypb.Empty) (*Info, error) GetEvents(*emptypb.Empty, GuestService_GetEventsServer) error PostInotify(GuestService_PostInotifyServer) error + Tunnel(GuestService_TunnelServer) error mustEmbedUnimplementedGuestServiceServer() } @@ -134,6 +167,9 @@ func (UnimplementedGuestServiceServer) GetEvents(*emptypb.Empty, GuestService_Ge func (UnimplementedGuestServiceServer) PostInotify(GuestService_PostInotifyServer) error { return status.Errorf(codes.Unimplemented, "method PostInotify not implemented") } +func (UnimplementedGuestServiceServer) Tunnel(GuestService_TunnelServer) error { + return status.Errorf(codes.Unimplemented, "method Tunnel not implemented") +} func (UnimplementedGuestServiceServer) mustEmbedUnimplementedGuestServiceServer() {} // UnsafeGuestServiceServer may be embedded to opt out of forward compatibility for this service. @@ -212,6 +248,32 @@ func (x *guestServicePostInotifyServer) Recv() (*Inotify, error) { return m, nil } +func _GuestService_Tunnel_Handler(srv interface{}, stream grpc.ServerStream) error { + return srv.(GuestServiceServer).Tunnel(&guestServiceTunnelServer{stream}) +} + +type GuestService_TunnelServer interface { + Send(*TunnelMessage) error + Recv() (*TunnelMessage, error) + grpc.ServerStream +} + +type guestServiceTunnelServer struct { + grpc.ServerStream +} + +func (x *guestServiceTunnelServer) Send(m *TunnelMessage) error { + return x.ServerStream.SendMsg(m) +} + +func (x *guestServiceTunnelServer) Recv() (*TunnelMessage, error) { + m := new(TunnelMessage) + if err := x.ServerStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + // GuestService_ServiceDesc is the grpc.ServiceDesc for GuestService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -235,6 +297,12 @@ var GuestService_ServiceDesc = grpc.ServiceDesc{ Handler: _GuestService_PostInotify_Handler, ClientStreams: true, }, + { + StreamName: "Tunnel", + Handler: _GuestService_Tunnel_Handler, + ServerStreams: true, + ClientStreams: true, + }, }, Metadata: "guestservice.proto", } diff --git a/pkg/guestagent/api/server/server.go b/pkg/guestagent/api/server/server.go index 93acc55b2b24..8c4fcde2b9ca 100644 --- a/pkg/guestagent/api/server/server.go +++ b/pkg/guestagent/api/server/server.go @@ -6,6 +6,7 @@ import ( "github.com/lima-vm/lima/pkg/guestagent" "github.com/lima-vm/lima/pkg/guestagent/api" + "github.com/lima-vm/lima/pkg/portfwdserver" "google.golang.org/grpc" "google.golang.org/protobuf/types/known/emptypb" ) @@ -18,14 +19,15 @@ func StartServer(lis net.Listener, guest *GuestServer) error { type GuestServer struct { api.UnimplementedGuestServiceServer - Agent guestagent.Agent + Agent guestagent.Agent + TunnelS *portfwdserver.TunnelServer } -func (s GuestServer) GetInfo(ctx context.Context, _ *emptypb.Empty) (*api.Info, error) { +func (s *GuestServer) GetInfo(ctx context.Context, _ *emptypb.Empty) (*api.Info, error) { return s.Agent.Info(ctx) } -func (s GuestServer) GetEvents(_ *emptypb.Empty, stream api.GuestService_GetEventsServer) error { +func (s *GuestServer) GetEvents(_ *emptypb.Empty, stream api.GuestService_GetEventsServer) error { responses := make(chan *api.Event) go s.Agent.Events(stream.Context(), responses) for response := range responses { @@ -37,7 +39,7 @@ func (s GuestServer) GetEvents(_ *emptypb.Empty, stream api.GuestService_GetEven return nil } -func (s GuestServer) PostInotify(server api.GuestService_PostInotifyServer) error { +func (s *GuestServer) PostInotify(server api.GuestService_PostInotifyServer) error { for { recv, err := server.Recv() if err != nil { @@ -46,3 +48,7 @@ func (s GuestServer) PostInotify(server api.GuestService_PostInotifyServer) erro s.Agent.HandleInotify(recv) } } + +func (s *GuestServer) Tunnel(stream api.GuestService_TunnelServer) error { + return s.TunnelS.Start(stream) +} diff --git a/pkg/guestagent/guestagent_linux.go b/pkg/guestagent/guestagent_linux.go index 2aed509583a5..e092d4915a33 100644 --- a/pkg/guestagent/guestagent_linux.go +++ b/pkg/guestagent/guestagent_linux.go @@ -224,16 +224,26 @@ func (a *agent) LocalPorts(_ context.Context) ([]*api.IPPort, error) { for _, f := range tcpParsed { switch f.Kind { case procnettcp.TCP, procnettcp.TCP6: + if f.State == procnettcp.TCPListen { + res = append(res, + &api.IPPort{ + Ip: f.IP.String(), + Port: int32(f.Port), + Protocol: "tcp", + }) + } + case procnettcp.UDP, procnettcp.UDP6: + if f.State == procnettcp.UDPEstablished { + res = append(res, + &api.IPPort{ + Ip: f.IP.String(), + Port: int32(f.Port), + Protocol: "udp", + }) + } default: continue } - if f.State == procnettcp.TCPListen { - res = append(res, - &api.IPPort{ - Ip: f.IP.String(), - Port: int32(f.Port), - }) - } } a.worthCheckingIPTablesMu.RLock() @@ -265,11 +275,14 @@ func (a *agent) LocalPorts(_ context.Context) ([]*api.IPPort, error) { } } if !found { - res = append(res, - &api.IPPort{ - Ip: ipt.IP.String(), - Port: int32(ipt.Port), - }) + if ipt.TCP { + res = append(res, + &api.IPPort{ + Ip: ipt.IP.String(), + Port: int32(ipt.Port), + Protocol: "tcp", + }) + } } } @@ -285,8 +298,9 @@ func (a *agent) LocalPorts(_ context.Context) ([]*api.IPPort, error) { if !found { res = append(res, &api.IPPort{ - Ip: entry.IP.String(), - Port: int32(entry.Port), + Ip: entry.IP.String(), + Port: int32(entry.Port), + Protocol: string(entry.Protocol), }) } } diff --git a/pkg/guestagent/procnettcp/procnettcp.go b/pkg/guestagent/procnettcp/procnettcp.go index 98b90b9c872f..01f94e7b7ccf 100644 --- a/pkg/guestagent/procnettcp/procnettcp.go +++ b/pkg/guestagent/procnettcp/procnettcp.go @@ -15,7 +15,9 @@ type Kind = string const ( TCP Kind = "tcp" TCP6 Kind = "tcp6" - // TODO: "udp", "udp6", "udplite", "udplite6". + UDP Kind = "udp" + UDP6 Kind = "udp6" + // TODO: "udplite", "udplite6". ) type State = int @@ -23,6 +25,7 @@ type State = int const ( TCPEstablished State = 0x1 TCPListen State = 0xA + UDPEstablished State = 0x7 ) type Entry struct { @@ -34,7 +37,7 @@ type Entry struct { func Parse(r io.Reader, kind Kind) ([]Entry, error) { switch kind { - case TCP, TCP6: + case TCP, TCP6, UDP, UDP6: default: return nil, fmt.Errorf("unexpected kind %q", kind) } diff --git a/pkg/guestagent/procnettcp/procnettcp_linux.go b/pkg/guestagent/procnettcp/procnettcp_linux.go index 690115317f7c..d1c632ee7a51 100644 --- a/pkg/guestagent/procnettcp/procnettcp_linux.go +++ b/pkg/guestagent/procnettcp/procnettcp_linux.go @@ -11,6 +11,8 @@ func ParseFiles() ([]Entry, error) { files := map[string]Kind{ "/proc/net/tcp": TCP, "/proc/net/tcp6": TCP6, + "/proc/net/udp": UDP, + "/proc/net/udp6": UDP6, } for file, kind := range files { r, err := os.Open(file) diff --git a/pkg/guestagent/procnettcp/procnettcp_test.go b/pkg/guestagent/procnettcp/procnettcp_test.go index 19134325c662..6f5c74ab2220 100644 --- a/pkg/guestagent/procnettcp/procnettcp_test.go +++ b/pkg/guestagent/procnettcp/procnettcp_test.go @@ -56,3 +56,19 @@ func TestParseTCP6Zero(t *testing.T) { assert.Equal(t, uint16(22), entries[0].Port) assert.Equal(t, TCPListen, entries[0].State) } + +func TestParseUDP(t *testing.T) { + procNetTCP := ` sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode ref pointer drops + 716: 3600007F:0035 00000000:0000 07 00000000:00000000 00:00000000 00000000 991 0 2964 2 0000000000000000 0 + 716: 3500007F:0035 00000000:0000 07 00000000:00000000 00:00000000 00000000 991 0 2962 2 0000000000000000 0 + 731: 0369A8C0:0044 00000000:0000 07 00000000:00000000 00:00000000 00000000 998 0 29132 2 0000000000000000 0 + 731: 0F05A8C0:0044 00000000:0000 07 00000000:00000000 00:00000000 00000000 998 0 4049 2 0000000000000000 0 + 1768: 00000000:1451 00000000:0000 07 00000000:00000000 00:00000000 00000000 502 0 28364 2 0000000000000000 0 ` + entries, err := Parse(strings.NewReader(procNetTCP), UDP) + assert.NilError(t, err) + t.Log(entries) + + assert.Check(t, net.ParseIP("127.0.0.54").Equal(entries[0].IP)) + assert.Equal(t, uint16(53), entries[0].Port) + assert.Equal(t, UDPEstablished, entries[0].State) +} diff --git a/pkg/hostagent/hostagent.go b/pkg/hostagent/hostagent.go index 0f09c6cee512..7fc3cbb523df 100644 --- a/pkg/hostagent/hostagent.go +++ b/pkg/hostagent/hostagent.go @@ -19,18 +19,18 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + "github.com/lima-vm/lima/pkg/cidata" "github.com/lima-vm/lima/pkg/driver" "github.com/lima-vm/lima/pkg/driverutil" - "github.com/lima-vm/lima/pkg/networks" - "github.com/lima-vm/lima/pkg/osutil" - - "github.com/lima-vm/lima/pkg/cidata" guestagentapi "github.com/lima-vm/lima/pkg/guestagent/api" guestagentclient "github.com/lima-vm/lima/pkg/guestagent/api/client" hostagentapi "github.com/lima-vm/lima/pkg/hostagent/api" "github.com/lima-vm/lima/pkg/hostagent/dns" "github.com/lima-vm/lima/pkg/hostagent/events" "github.com/lima-vm/lima/pkg/limayaml" + "github.com/lima-vm/lima/pkg/networks" + "github.com/lima-vm/lima/pkg/osutil" + "github.com/lima-vm/lima/pkg/portfwd" "github.com/lima-vm/lima/pkg/sshutil" "github.com/lima-vm/lima/pkg/store" "github.com/lima-vm/lima/pkg/store/filenames" @@ -40,16 +40,18 @@ import ( ) type HostAgent struct { - y *limayaml.LimaYAML - sshLocalPort int - udpDNSLocalPort int - tcpDNSLocalPort int - instDir string - instName string - instSSHAddress string - sshConfig *ssh.SSHConfig - portForwarder *portForwarder - onClose []func() error // LIFO + y *limayaml.LimaYAML + sshLocalPort int + udpDNSLocalPort int + tcpDNSLocalPort int + instDir string + instName string + instSSHAddress string + sshConfig *ssh.SSHConfig + portForwarder *portForwarder + grpcPortForwarder *portfwd.Forwarder + + onClose []func() error // LIFO driver driver.Driver signalCh chan os.Signal @@ -164,6 +166,11 @@ func New(instName string, stdout io.Writer, signalCh chan os.Signal, opts ...Opt limayaml.FillPortForwardDefaults(&rule, inst.Dir, inst.Param) rules = append(rules, rule) + env, _ := strconv.ParseBool(os.Getenv("LIMA_SSH_PORT_FORWARDER")) + if !env { + logrus.Warn("GRPC port forwarding is experimental") + } + limaDriver := driverutil.CreateTargetDriverInstance(&driver.BaseDriver{ Instance: inst, Yaml: y, @@ -182,6 +189,7 @@ func New(instName string, stdout io.Writer, signalCh chan os.Signal, opts ...Opt instSSHAddress: inst.SSHAddress, sshConfig: sshConfig, portForwarder: newPortForwarder(sshConfig, sshLocalPort, rules, inst.VMType), + grpcPortForwarder: portfwd.NewPortForwarder(rules), driver: limaDriver, signalCh: signalCh, eventEnc: json.NewEncoder(stdout), @@ -671,7 +679,12 @@ func (a *HostAgent) processGuestAgentEvents(ctx context.Context, client *guestag for _, f := range ev.Errors { logrus.Warnf("received error from the guest: %q", f) } - a.portForwarder.OnEvent(ctx, ev) + env, _ := strconv.ParseBool(os.Getenv("LIMA_SSH_PORT_FORWARDER")) + if env { + a.portForwarder.OnEvent(ctx, ev) + } else { + a.grpcPortForwarder.OnEvent(ctx, client, ev) + } } if err := client.Events(ctx, onEvent); err != nil { diff --git a/pkg/hostagent/port.go b/pkg/hostagent/port.go index 36d0ea86f736..4c39b953d508 100644 --- a/pkg/hostagent/port.go +++ b/pkg/hostagent/port.go @@ -76,6 +76,9 @@ func (pf *portForwarder) forwardingAddresses(guest *api.IPPort) (hostAddr, guest func (pf *portForwarder) OnEvent(ctx context.Context, ev *api.Event) { for _, f := range ev.LocalPortsRemoved { + if f.Protocol != "tcp" { + continue + } local, remote := pf.forwardingAddresses(f) if local == "" { continue @@ -86,6 +89,9 @@ func (pf *portForwarder) OnEvent(ctx context.Context, ev *api.Event) { } } for _, f := range ev.LocalPortsAdded { + if f.Protocol != "tcp" { + continue + } local, remote := pf.forwardingAddresses(f) if local == "" { logrus.Infof("Not forwarding TCP %s", remote) diff --git a/pkg/portfwd/client.go b/pkg/portfwd/client.go new file mode 100644 index 000000000000..b7932bd8db74 --- /dev/null +++ b/pkg/portfwd/client.go @@ -0,0 +1,156 @@ +package portfwd + +import ( + "errors" + "fmt" + "io" + "net" + + "github.com/lima-vm/lima/pkg/guestagent/api" + guestagentclient "github.com/lima-vm/lima/pkg/guestagent/api/client" + "github.com/sirupsen/logrus" + + "golang.org/x/net/context" +) + +func HandleTCPConnection(ctx context.Context, client *guestagentclient.GuestAgentClient, conn net.Conn, guestAddr string) { + defer conn.Close() + + id := fmt.Sprintf("tcp-%s-%s", conn.LocalAddr().String(), conn.RemoteAddr().String()) + errCh := make(chan error, 2) + + stream, err := client.Tunnel(ctx) + if err != nil { + logrus.Errorf("could not open tcp tunnel for id: %s error:%v", id, err) + } + + rw := &GrpcClientRW{stream: stream, id: id, addr: guestAddr} + go func() { + _, err := io.Copy(rw, conn) + if errors.Is(err, io.EOF) { + errCh <- nil + return + } + errCh <- err + }() + go func() { + _, err := io.Copy(conn, rw) + if errors.Is(err, io.EOF) { + errCh <- nil + return + } + errCh <- err + }() + + err = <-errCh + if err != nil { + logrus.Debugf("error in tcp tunnel for id: %s error:%v", id, err) + } +} + +func HandleUDPConnection(ctx context.Context, client *guestagentclient.GuestAgentClient, conn net.PacketConn, guestAddr string) { + defer conn.Close() + + id := fmt.Sprintf("udp-%s", conn.LocalAddr().String()) + + stream, err := client.Tunnel(ctx) + if err != nil { + logrus.Errorf("could not open udp tunnel for id: %s error:%v", id, err) + } + + errCh := make(chan error, 2) + + go func() { + buf := make([]byte, 65507) + for { + n, addr, err := conn.ReadFrom(buf) + if errors.Is(err, io.EOF) { + errCh <- nil + return + } + if err != nil { + errCh <- err + return + } + msg := &api.TunnelMessage{ + Id: id + "-" + addr.String(), + Protocol: "udp", + GuestAddr: guestAddr, + Data: buf[:n], + UdpTargetAddr: addr.String(), + } + if err := stream.Send(msg); err != nil { + errCh <- err + return + } + } + }() + + go func() { + for { + in, err := stream.Recv() + if errors.Is(err, io.EOF) { + errCh <- nil + return + } + if err != nil { + errCh <- err + return + } + addr, err := net.ResolveUDPAddr("udp", in.UdpTargetAddr) + if err != nil { + errCh <- err + return + } + _, err = conn.WriteTo(in.Data, addr) + if err != nil { + errCh <- err + return + } + } + }() + + err = <-errCh + if err != nil { + logrus.Debugf("error in udp tunnel for id: %s error:%v", id, err) + } +} + +type GrpcClientRW struct { + id string + addr string + stream api.GuestService_TunnelClient +} + +var _ io.ReadWriter = (*GrpcClientRW)(nil) + +func (g GrpcClientRW) Write(p []byte) (n int, err error) { + if len(p) == 0 { + return 0, nil + } + err = g.stream.Send(&api.TunnelMessage{ + Id: g.id, + GuestAddr: g.addr, + Data: p, + Protocol: "tcp", + }) + if err != nil { + return 0, err + } + return len(p), nil +} + +func (g GrpcClientRW) Read(p []byte) (n int, err error) { + in, err := g.stream.Recv() + if errors.Is(err, io.EOF) { + return 0, nil + } + if err != nil { + return 0, err + } + if len(in.Data) == 0 { + return 0, nil + } + copy(p, in.Data) + return len(in.Data), nil +} diff --git a/pkg/portfwd/control_others.go b/pkg/portfwd/control_others.go new file mode 100644 index 000000000000..82f1c890687c --- /dev/null +++ b/pkg/portfwd/control_others.go @@ -0,0 +1,27 @@ +//go:build !windows + +package portfwd + +import ( + "syscall" + + "golang.org/x/sys/unix" +) + +func Control(_, _ string, c syscall.RawConn) (err error) { + controlErr := c.Control(func(fd uintptr) { + err = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEADDR, 1) + if err != nil { + return + } + + err = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1) + if err != nil { + return + } + }) + if controlErr != nil { + err = controlErr + } + return +} diff --git a/pkg/portfwd/control_windows.go b/pkg/portfwd/control_windows.go new file mode 100644 index 000000000000..d9e3dc04ca2b --- /dev/null +++ b/pkg/portfwd/control_windows.go @@ -0,0 +1,21 @@ +//go:build windows + +package portfwd + +import ( + "golang.org/x/sys/windows" + "syscall" +) + +func Control(_, _ string, c syscall.RawConn) (err error) { + controlErr := c.Control(func(fd uintptr) { + err = windows.SetsockoptInt(windows.Handle(int(fd)), windows.SOL_SOCKET, windows.SO_REUSEADDR, 1) + if err != nil { + return + } + }) + if controlErr != nil { + err = controlErr + } + return +} diff --git a/pkg/portfwd/forward.go b/pkg/portfwd/forward.go new file mode 100644 index 000000000000..cf616d8de4d0 --- /dev/null +++ b/pkg/portfwd/forward.go @@ -0,0 +1,87 @@ +package portfwd + +import ( + "context" + "net" + "strings" + + "github.com/lima-vm/lima/pkg/guestagent/api" + guestagentclient "github.com/lima-vm/lima/pkg/guestagent/api/client" + "github.com/lima-vm/lima/pkg/limayaml" + "github.com/sirupsen/logrus" +) + +var IPv4loopback1 = limayaml.IPv4loopback1 + +type Forwarder struct { + rules []limayaml.PortForward + closableListeners *ClosableListeners +} + +func NewPortForwarder(rules []limayaml.PortForward) *Forwarder { + return &Forwarder{rules: rules, closableListeners: NewClosableListener()} +} + +func (fw *Forwarder) OnEvent(ctx context.Context, client *guestagentclient.GuestAgentClient, ev *api.Event) { + for _, f := range ev.LocalPortsAdded { + local, remote := fw.forwardingAddresses(f) + if local == "" { + logrus.Infof("Not forwarding %s %s", strings.ToUpper(f.Protocol), remote) + continue + } + logrus.Infof("Forwarding %s from %s to %s", strings.ToUpper(f.Protocol), remote, local) + fw.closableListeners.Forward(ctx, client, f.Protocol, local, remote) + } + for _, f := range ev.LocalPortsRemoved { + local, remote := fw.forwardingAddresses(f) + if local == "" { + continue + } + fw.closableListeners.Remove(ctx, f.Protocol, local, remote) + logrus.Debugf("Port forwarding closed proto:%s host:%s guest:%s", f.Protocol, local, remote) + } +} + +func (fw *Forwarder) forwardingAddresses(guest *api.IPPort) (hostAddr, guestAddr string) { + guestIP := net.ParseIP(guest.Ip) + for _, rule := range fw.rules { + if rule.GuestSocket != "" { + continue + } + if guest.Port < int32(rule.GuestPortRange[0]) || guest.Port > int32(rule.GuestPortRange[1]) { + continue + } + switch { + case guestIP.IsUnspecified(): + case guestIP.Equal(rule.GuestIP): + case guestIP.Equal(net.IPv6loopback) && rule.GuestIP.Equal(IPv4loopback1): + case rule.GuestIP.IsUnspecified() && !rule.GuestIPMustBeZero: + // When GuestIPMustBeZero is true, then 0.0.0.0 must be an exact match, which is already + // handled above by the guest.IP.IsUnspecified() condition. + default: + continue + } + if rule.Ignore { + if guestIP.IsUnspecified() && !rule.GuestIP.IsUnspecified() { + continue + } + break + } + return hostAddress(rule, guest), guest.HostString() + } + return "", guest.HostString() +} + +func hostAddress(rule limayaml.PortForward, guest *api.IPPort) string { + if rule.HostSocket != "" { + return rule.HostSocket + } + host := &api.IPPort{Ip: rule.HostIP.String()} + if guest.Port == 0 { + // guest is a socket + host.Port = int32(rule.HostPort) + } else { + host.Port = guest.Port + int32(rule.HostPortRange[0]-rule.GuestPortRange[0]) + } + return host.HostString() +} diff --git a/pkg/portfwd/listener.go b/pkg/portfwd/listener.go new file mode 100644 index 000000000000..b5649ddaeaad --- /dev/null +++ b/pkg/portfwd/listener.go @@ -0,0 +1,119 @@ +package portfwd + +import ( + "context" + "fmt" + "net" + "sync" + + guestagentclient "github.com/lima-vm/lima/pkg/guestagent/api/client" + "github.com/sirupsen/logrus" +) + +type ClosableListeners struct { + listenConfig net.ListenConfig + listeners map[string]net.Listener + udpListeners map[string]net.PacketConn + listenersRW sync.Mutex + udpListenersRW sync.Mutex +} + +func NewClosableListener() *ClosableListeners { + listenConfig := net.ListenConfig{ + Control: Control, + } + + return &ClosableListeners{ + listeners: make(map[string]net.Listener), + udpListeners: make(map[string]net.PacketConn), + listenConfig: listenConfig, + } +} + +func (p *ClosableListeners) Forward(ctx context.Context, client *guestagentclient.GuestAgentClient, + protocol string, hostAddress string, guestAddress string, +) { + switch protocol { + case "tcp", "tcp6": + go p.forwardTCP(ctx, client, hostAddress, guestAddress) + case "udp", "udp6": + go p.forwardUDP(ctx, client, hostAddress, guestAddress) + } +} + +func (p *ClosableListeners) Remove(_ context.Context, protocol, hostAddress, guestAddress string) { + key := key(protocol, hostAddress, guestAddress) + switch protocol { + case "tcp", "tcp6": + p.listenersRW.Lock() + defer p.listenersRW.Unlock() + listener, ok := p.listeners[key] + if ok { + listener.Close() + delete(p.listeners, key) + } + case "udp", "udp6": + p.udpListenersRW.Lock() + defer p.udpListenersRW.Unlock() + listener, ok := p.udpListeners[key] + if ok { + listener.Close() + delete(p.udpListeners, key) + } + } +} + +func (p *ClosableListeners) forwardTCP(ctx context.Context, client *guestagentclient.GuestAgentClient, hostAddress, guestAddress string) { + key := key("tcp", hostAddress, guestAddress) + defer p.Remove(ctx, "tcp", hostAddress, guestAddress) + + p.listenersRW.Lock() + _, ok := p.listeners[key] + if ok { + p.listenersRW.Unlock() + return + } + tcpLis, err := Listen(ctx, p.listenConfig, hostAddress) + if err != nil { + logrus.Errorf("failed to accept TCP connection: %v", err) + p.listenersRW.Unlock() + return + } + p.listeners[key] = tcpLis + p.listenersRW.Unlock() + for { + conn, err := tcpLis.Accept() + if err != nil { + logrus.Errorf("failed to accept TCP connection: %v", err) + return + } + go HandleTCPConnection(ctx, client, conn, guestAddress) + } +} + +func (p *ClosableListeners) forwardUDP(ctx context.Context, client *guestagentclient.GuestAgentClient, hostAddress, guestAddress string) { + key := key("udp", hostAddress, guestAddress) + defer p.Remove(ctx, "udp", hostAddress, guestAddress) + + p.udpListenersRW.Lock() + _, ok := p.udpListeners[key] + if ok { + p.udpListenersRW.Unlock() + return + } + + udpConn, err := ListenPacket(ctx, p.listenConfig, hostAddress) + if err != nil { + logrus.Errorf("failed to listen udp: %v", err) + p.udpListenersRW.Unlock() + return + } + p.udpListeners[key] = udpConn + p.udpListenersRW.Unlock() + + HandleUDPConnection(ctx, client, udpConn, guestAddress) +} + +func key(protocol, hostAddress, guestAddress string) string { + return fmt.Sprintf("%s-%s-%s", protocol, hostAddress, guestAddress) +} diff --git a/pkg/portfwd/listener_others.go b/pkg/portfwd/listener_others.go new file mode 100644 index 000000000000..74473f7b5f8d --- /dev/null +++ b/pkg/portfwd/listener_others.go @@ -0,0 +1,16 @@ +//go:build !darwin + +package portfwd + +import ( + "context" + "net" +) + +func Listen(ctx context.Context, listenConfig net.ListenConfig, hostAddress string) (net.Listener, error) { + return listenConfig.Listen(ctx, "tcp", hostAddress) +} + +func ListenPacket(ctx context.Context, listenConfig net.ListenConfig, hostAddress string) (net.PacketConn, error) { + return listenConfig.ListenPacket(ctx, "udp", hostAddress) +} diff --git a/pkg/portfwd/listerner_darwin.go b/pkg/portfwd/listerner_darwin.go new file mode 100644 index 000000000000..3c1d080e747a --- /dev/null +++ b/pkg/portfwd/listerner_darwin.go @@ -0,0 +1,107 @@ +package portfwd + +import ( + "context" + "fmt" + "net" + "strconv" + + "github.com/sirupsen/logrus" +) + +func Listen(ctx context.Context, listenConfig net.ListenConfig, hostAddress string) (net.Listener, error) { + localIPStr, localPortStr, _ := net.SplitHostPort(hostAddress) + localIP := net.ParseIP(localIPStr) + localPort, _ := strconv.Atoi(localPortStr) + + if !localIP.Equal(IPv4loopback1) || localPort >= 1024 { + tcpLis, err := listenConfig.Listen(ctx, "tcp", hostAddress) + if err != nil { + logrus.Errorf("failed to listen tcp: %v", err) + return nil, err + } + return tcpLis, nil + } + tcpLis, err := listenConfig.Listen(ctx, "tcp", fmt.Sprintf("0.0.0.0:%d", localPort)) + if err != nil { + logrus.Errorf("failed to listen tcp: %v", err) + return nil, err + } + return &pseudoLoopbackListener{tcpLis}, nil +} + +func ListenPacket(ctx context.Context, listenConfig net.ListenConfig, hostAddress string) (net.PacketConn, error) { + localIPStr, localPortStr, _ := net.SplitHostPort(hostAddress) + localIP := net.ParseIP(localIPStr) + localPort, _ := strconv.Atoi(localPortStr) + + if !localIP.Equal(IPv4loopback1) || localPort >= 1024 { + udpConn, err := listenConfig.ListenPacket(ctx, "udp", hostAddress) + if err != nil { + logrus.Errorf("failed to listen udp: %v", err) + return nil, err + } + return udpConn, nil + } + udpConn, err := listenConfig.ListenPacket(ctx, "udp", fmt.Sprintf("0.0.0.0:%d", localPort)) + if err != nil { + logrus.Errorf("failed to listen udp: %v", err) + return nil, err + } + return &pseudoLoopbackPacketConn{udpConn}, nil +} + +type pseudoLoopbackListener struct { + net.Listener +} + +func (p pseudoLoopbackListener) Accept() (net.Conn, error) { + conn, err := p.Listener.Accept() + if err != nil { + return nil, err + } + + remoteAddr := conn.RemoteAddr().String() // ip:port + remoteAddrIP, _, err := net.SplitHostPort(remoteAddr) + if err != nil { + logrus.WithError(err).Debugf("pseudoloopback forwarder: rejecting non-loopback remoteAddr %q (unparsable)", remoteAddr) + conn.Close() + return nil, err + } + if remoteAddrIP != "127.0.0.1" { + logrus.WithError(err).Debugf("pseudoloopback forwarder: rejecting non-loopback remoteAddr %q", remoteAddr) + return nil, err + } + return conn, nil +} + +type pseudoLoopbackPacketConn struct { + net.PacketConn +} + +func (pk *pseudoLoopbackPacketConn) ReadFrom(bytes []byte) (n int, addr net.Addr, err error) { + n, remoteAddr, err := pk.PacketConn.ReadFrom(bytes) + if err != nil { + return 0, nil, err + } + + remoteAddrIP, _, err := net.SplitHostPort(remoteAddr.String()) + if err != nil { + return 0, nil, err + } + if remoteAddrIP != "127.0.0.1" { + return 0, nil, fmt.Errorf("pseudoloopback forwarder: rejecting non-loopback remoteAddr %q", remoteAddr) + } + return n, remoteAddr, nil +} + +func (pk *pseudoLoopbackPacketConn) WriteTo(bytes []byte, remoteAddr net.Addr) (n int, err error) { + remoteAddrIP, _, err := net.SplitHostPort(remoteAddr.String()) + if err != nil { + return 0, err + } + if remoteAddrIP != "127.0.0.1" { + return 0, fmt.Errorf("pseudoloopback forwarder: rejecting non-loopback remoteAddr %q", remoteAddr) + } + return pk.PacketConn.WriteTo(bytes, remoteAddr) +} diff --git a/pkg/portfwdserver/server.go b/pkg/portfwdserver/server.go new file mode 100644 index 000000000000..314575e55fee --- /dev/null +++ b/pkg/portfwdserver/server.go @@ -0,0 +1,69 @@ +package portfwdserver + +import ( + "errors" + "io" + "net" + + "github.com/lima-vm/lima/pkg/guestagent/api" +) + +type TunnelServer struct { + Conns map[string]net.Conn +} + +func NewTunnelServer() *TunnelServer { + return &TunnelServer{ + Conns: make(map[string]net.Conn), + } +} + +func (s *TunnelServer) Start(stream api.GuestService_TunnelServer) error { + for { + in, err := stream.Recv() + if errors.Is(err, io.EOF) { + return nil + } + if err != nil { + return err + } + if len(in.Data) == 0 { + continue + } + + conn, ok := s.Conns[in.Id] + if !ok { + conn, err = net.Dial(in.Protocol, in.GuestAddr) + if err != nil { + return err + } + s.Conns[in.Id] = conn + + writer := &GRPCServerWriter{id: in.Id, udpAddr: in.UdpTargetAddr, stream: stream} + go func() { + _, _ = io.Copy(writer, conn) + delete(s.Conns, writer.id) + }() + } + _, err = conn.Write(in.Data) + if err != nil { + return err + } + } +} + +type GRPCServerWriter struct { + id string + udpAddr string + stream api.GuestService_TunnelServer +} + +var _ io.Writer = (*GRPCServerWriter)(nil) + +func (g GRPCServerWriter) Write(p []byte) (n int, err error) { + if len(p) == 0 { + return 0, nil + } + err = g.stream.Send(&api.TunnelMessage{Id: g.id, Data: p, UdpTargetAddr: g.udpAddr}) + return len(p), err +} diff --git a/website/content/en/docs/config/Port/_index.md b/website/content/en/docs/config/Port/_index.md new file mode 100644 index 000000000000..c2922e1352cc --- /dev/null +++ b/website/content/en/docs/config/Port/_index.md @@ -0,0 +1,65 @@ +--- +title: Port Forwarding +weight: 50 +--- + +Lima supports automatic port-forwarding of localhost ports from guest to host. + +## Port forwarding types + +### Using SSH + +SSH based port forwarding is the default and current model that is supported in Lima prior to v1.0. + +To use SSH forwarding use the below command + +```bash +LIMA_SSH_PORT_FORWARDER=true limactl start +``` + +#### Caveats + +- Doesn't support UDP based port forwarding +- Spans child process on host for running SSH master. + +### Using GRPC (Default since Lima v1.0) + +> **Warning** +> This mode is experimental + +| ⚡ Requirement | Lima >= 1.0 | +|---------------|-------------| + +In this model, lima uses existing GRPC communication (Host <-> Guest) to tunnel port forwarding requests. +For each port forwarding request, a GRPC tunnel is created and this will be used for transmitting data + +To disable this feature and use SSH forwarding use the following environment variable + +```bash +LIMA_SSH_PORT_FORWARDER=true limactl start +``` + +#### Advantages + +- Supports both TCP and UDP based port forwarding +- Performs faster compared to SSH based forwarding +- No additional child process for port forwarding + +### Benchmarks + +| Usecase | GRPC | SSH | +|-------------|----------------|----------------| +| TCP | 3.80 Gbits/sec | 3.38 Gbits/sec | +| TCP Reverse | 4.77 Gbits/sec | 3.08 Gbits/sec | + +The benchmarks detail above are obtained using the following commands + +``` +Host -> limactl start vz + +VZ Guest -> iperf3 -s + +Host -> iperf3 -c 127.0.0.1 //Benchmark for TCP +Host -> iperf3 -c 127.0.0.1 -R //Benchmark for TCP Reverse +``` +