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
+```
+