From a78814a2e9fef204d0fb06fa0b0b7ae0d8b39589 Mon Sep 17 00:00:00 2001
From: fatedier
Date: Wed, 16 Apr 2025 16:05:54 +0800
Subject: [PATCH 1/2] virtual-net: initial (#4751)
---
README.md | 45 ++-
README_zh.md | 5 -
Release.md | 11 +-
client/control.go | 8 +-
client/proxy/proxy.go | 29 +-
client/proxy/proxy_manager.go | 6 +-
client/proxy/proxy_wrapper.go | 7 +-
client/service.go | 32 +-
client/visitor/stcp.go | 4 +
client/visitor/visitor.go | 34 +-
client/visitor/visitor_manager.go | 14 +-
client/visitor/xtcp.go | 10 +-
cmd/frpc/sub/root.go | 7 +
conf/frpc_full_example.toml | 26 ++
doc/virtual_net.md | 75 ++++
go.mod | 25 +-
go.sum | 65 ++--
pkg/config/v1/client.go | 15 +-
pkg/config/v1/{plugin.go => proxy_plugin.go} | 56 +--
pkg/config/v1/validation/client.go | 8 +
pkg/config/v1/visitor.go | 3 +
pkg/config/v1/visitor_plugin.go | 86 +++++
pkg/featuregate/feature_gate.go | 219 +++++++++++
pkg/plugin/client/http2http.go | 10 +-
pkg/plugin/client/http2https.go | 10 +-
pkg/plugin/client/http_proxy.go | 8 +-
pkg/plugin/client/https2http.go | 14 +-
pkg/plugin/client/https2https.go | 14 +-
pkg/plugin/client/plugin.go | 26 +-
pkg/plugin/client/socks5.go | 11 +-
pkg/plugin/client/static_file.go | 10 +-
pkg/plugin/client/tls2raw.go | 9 +-
pkg/plugin/client/unix_domain_socket.go | 13 +-
pkg/plugin/client/virtual_net.go | 71 ++++
pkg/plugin/server/http.go | 2 +-
pkg/plugin/server/manager.go | 2 +-
pkg/plugin/server/plugin.go | 2 +-
pkg/plugin/server/tracer.go | 2 +-
pkg/plugin/server/types.go | 2 +-
pkg/plugin/visitor/plugin.go | 58 +++
pkg/plugin/visitor/virtual_net.go | 232 ++++++++++++
pkg/util/version/version.go | 2 +-
pkg/vnet/controller.go | 360 +++++++++++++++++++
pkg/vnet/message.go | 81 +++++
pkg/vnet/tun.go | 77 ++++
pkg/vnet/tun_darwin.go | 85 +++++
pkg/vnet/tun_linux.go | 84 +++++
pkg/vnet/tun_unsupported.go | 27 ++
48 files changed, 1822 insertions(+), 180 deletions(-)
create mode 100644 doc/virtual_net.md
rename pkg/config/v1/{plugin.go => proxy_plugin.go} (96%)
create mode 100644 pkg/config/v1/visitor_plugin.go
create mode 100644 pkg/featuregate/feature_gate.go
create mode 100644 pkg/plugin/client/virtual_net.go
create mode 100644 pkg/plugin/visitor/plugin.go
create mode 100644 pkg/plugin/visitor/virtual_net.go
create mode 100644 pkg/vnet/controller.go
create mode 100644 pkg/vnet/message.go
create mode 100644 pkg/vnet/tun.go
create mode 100644 pkg/vnet/tun_darwin.go
create mode 100644 pkg/vnet/tun_linux.go
create mode 100644 pkg/vnet/tun_unsupported.go
diff --git a/README.md b/README.md
index ab9ef37b..e604d5af 100644
--- a/README.md
+++ b/README.md
@@ -18,11 +18,6 @@ frp is an open source project with its ongoing development made possible entirel
-
-
-
-
-
@@ -97,6 +92,11 @@ frp also offers a P2P connect mode.
* [Client Plugins](#client-plugins)
* [Server Manage Plugins](#server-manage-plugins)
* [SSH Tunnel Gateway](#ssh-tunnel-gateway)
+ * [Virtual Network (VirtualNet)](#virtual-network-virtualnet)
+* [Feature Gates](#feature-gates)
+ * [Available Feature Gates](#available-feature-gates)
+ * [Enabling Feature Gates](#enabling-feature-gates)
+ * [Feature Lifecycle](#feature-lifecycle)
* [Related Projects](#related-projects)
* [Contributing](#contributing)
* [Donation](#donation)
@@ -1260,6 +1260,41 @@ frpc tcp --proxy_name "test-tcp" --local_ip 127.0.0.1 --local_port 8080 --remote
Please refer to this [document](/doc/ssh_tunnel_gateway.md) for more information.
+### Virtual Network (VirtualNet)
+
+*Alpha feature added in v0.62.0*
+
+The VirtualNet feature enables frp to create and manage virtual network connections between clients and visitors through a TUN interface. This allows for IP-level routing between machines, extending frp beyond simple port forwarding to support full network connectivity.
+
+For detailed information about configuration and usage, please refer to the [VirtualNet documentation](/doc/virtual_net.md).
+
+## Feature Gates
+
+frp supports feature gates to enable or disable experimental features. This allows users to try out new features before they're considered stable.
+
+### Available Feature Gates
+
+| Name | Stage | Default | Description |
+|------|-------|---------|-------------|
+| VirtualNet | ALPHA | false | Virtual network capabilities for frp |
+
+### Enabling Feature Gates
+
+To enable an experimental feature, add the feature gate to your configuration:
+
+```toml
+featureGates = {
+ VirtualNet = true
+}
+```
+
+### Feature Lifecycle
+
+Features typically go through three stages:
+1. **ALPHA**: Disabled by default, may be unstable
+2. **BETA**: May be enabled by default, more stable but still evolving
+3. **GA (Generally Available)**: Enabled by default, ready for production use
+
## Related Projects
* [gofrp/plugin](https://github.com/gofrp/plugin) - A repository for frp plugins that contains a variety of plugins implemented based on the frp extension mechanism, meeting the customization needs of different scenarios.
diff --git a/README_zh.md b/README_zh.md
index ebd3109b..d911e152 100644
--- a/README_zh.md
+++ b/README_zh.md
@@ -20,11 +20,6 @@ frp 是一个完全开源的项目,我们的开发工作完全依靠赞助者
-
-
-
-
-
diff --git a/Release.md b/Release.md
index 11b788a6..b3edf410 100644
--- a/Release.md
+++ b/Release.md
@@ -1,7 +1,8 @@
+### Notes
+
+* **Feature Gates Introduced:** This version introduces a new experimental mechanism called Feature Gates. This allows users to enable or disable specific experimental features before they become generally available. Feature gates can be configured in the `featureGates` map within the configuration file.
+* **VirtualNet Feature Gate:** The first available feature gate is `VirtualNet`, which enables the experimental Virtual Network functionality (currently in Alpha stage).
+
### Features
-* Support metadatas and annotations in frpc proxy commands.
-
-### Fixes
-
-* Properly release resources in service.Close() to prevent resource leaks when used as a library.
+* **Virtual Network (VirtualNet):** Introduce experimental virtual network capabilities (Alpha). This allows creating a TUN device managed by frp, enabling Layer 3 connectivity between different clients within the frp network. Requires root/admin privileges and is currently supported on Linux and macOS. Configuration is done via the `virtualNet` section and the `virtual_net` plugin. Enable with feature gate `VirtualNet`. **Note: As an Alpha feature, configuration details may change in future releases.**
\ No newline at end of file
diff --git a/client/control.go b/client/control.go
index 3e20c312..157b4aef 100644
--- a/client/control.go
+++ b/client/control.go
@@ -29,6 +29,7 @@ import (
netpkg "github.com/fatedier/frp/pkg/util/net"
"github.com/fatedier/frp/pkg/util/wait"
"github.com/fatedier/frp/pkg/util/xlog"
+ "github.com/fatedier/frp/pkg/vnet"
)
type SessionContext struct {
@@ -46,6 +47,8 @@ type SessionContext struct {
AuthSetter auth.Setter
// Connector is used to create new connections, which could be real TCP connections or virtual streams.
Connector Connector
+ // Virtual net controller
+ VnetController *vnet.Controller
}
type Control struct {
@@ -99,8 +102,9 @@ func NewControl(ctx context.Context, sessionCtx *SessionContext) (*Control, erro
ctl.registerMsgHandlers()
ctl.msgTransporter = transport.NewMessageTransporter(ctl.msgDispatcher.SendChannel())
- ctl.pm = proxy.NewManager(ctl.ctx, sessionCtx.Common, ctl.msgTransporter)
- ctl.vm = visitor.NewManager(ctl.ctx, sessionCtx.RunID, sessionCtx.Common, ctl.connectServer, ctl.msgTransporter)
+ ctl.pm = proxy.NewManager(ctl.ctx, sessionCtx.Common, ctl.msgTransporter, sessionCtx.VnetController)
+ ctl.vm = visitor.NewManager(ctl.ctx, sessionCtx.RunID, sessionCtx.Common,
+ ctl.connectServer, ctl.msgTransporter, sessionCtx.VnetController)
return ctl, nil
}
diff --git a/client/proxy/proxy.go b/client/proxy/proxy.go
index 5cb5ccf2..debda9fa 100644
--- a/client/proxy/proxy.go
+++ b/client/proxy/proxy.go
@@ -36,6 +36,7 @@ import (
"github.com/fatedier/frp/pkg/transport"
"github.com/fatedier/frp/pkg/util/limit"
"github.com/fatedier/frp/pkg/util/xlog"
+ "github.com/fatedier/frp/pkg/vnet"
)
var proxyFactoryRegistry = map[reflect.Type]func(*BaseProxy, v1.ProxyConfigurer) Proxy{}
@@ -58,6 +59,7 @@ func NewProxy(
pxyConf v1.ProxyConfigurer,
clientCfg *v1.ClientCommonConfig,
msgTransporter transport.MessageTransporter,
+ vnetController *vnet.Controller,
) (pxy Proxy) {
var limiter *rate.Limiter
limitBytes := pxyConf.GetBaseConfig().Transport.BandwidthLimit.Bytes()
@@ -70,6 +72,7 @@ func NewProxy(
clientCfg: clientCfg,
limiter: limiter,
msgTransporter: msgTransporter,
+ vnetController: vnetController,
xl: xlog.FromContextSafe(ctx),
ctx: ctx,
}
@@ -85,6 +88,7 @@ type BaseProxy struct {
baseCfg *v1.ProxyBaseConfig
clientCfg *v1.ClientCommonConfig
msgTransporter transport.MessageTransporter
+ vnetController *vnet.Controller
limiter *rate.Limiter
// proxyPlugin is used to handle connections instead of dialing to local service.
// It's only validate for TCP protocol now.
@@ -98,7 +102,10 @@ type BaseProxy struct {
func (pxy *BaseProxy) Run() error {
if pxy.baseCfg.Plugin.Type != "" {
- p, err := plugin.Create(pxy.baseCfg.Plugin.Type, pxy.baseCfg.Plugin.ClientPluginOptions)
+ p, err := plugin.Create(pxy.baseCfg.Plugin.Type, plugin.PluginContext{
+ Name: pxy.baseCfg.Name,
+ VnetController: pxy.vnetController,
+ }, pxy.baseCfg.Plugin.ClientPluginOptions)
if err != nil {
return err
}
@@ -157,22 +164,22 @@ func (pxy *BaseProxy) HandleTCPWorkConnection(workConn net.Conn, m *msg.StartWor
}
// check if we need to send proxy protocol info
- var extraInfo plugin.ExtraInfo
+ var connInfo plugin.ConnectionInfo
if m.SrcAddr != "" && m.SrcPort != 0 {
if m.DstAddr == "" {
m.DstAddr = "127.0.0.1"
}
srcAddr, _ := net.ResolveTCPAddr("tcp", net.JoinHostPort(m.SrcAddr, strconv.Itoa(int(m.SrcPort))))
dstAddr, _ := net.ResolveTCPAddr("tcp", net.JoinHostPort(m.DstAddr, strconv.Itoa(int(m.DstPort))))
- extraInfo.SrcAddr = srcAddr
- extraInfo.DstAddr = dstAddr
+ connInfo.SrcAddr = srcAddr
+ connInfo.DstAddr = dstAddr
}
if baseCfg.Transport.ProxyProtocolVersion != "" && m.SrcAddr != "" && m.SrcPort != 0 {
h := &pp.Header{
Command: pp.PROXY,
- SourceAddr: extraInfo.SrcAddr,
- DestinationAddr: extraInfo.DstAddr,
+ SourceAddr: connInfo.SrcAddr,
+ DestinationAddr: connInfo.DstAddr,
}
if strings.Contains(m.SrcAddr, ".") {
@@ -186,13 +193,15 @@ func (pxy *BaseProxy) HandleTCPWorkConnection(workConn net.Conn, m *msg.StartWor
} else if baseCfg.Transport.ProxyProtocolVersion == "v2" {
h.Version = 2
}
- extraInfo.ProxyProtocolHeader = h
+ connInfo.ProxyProtocolHeader = h
}
+ connInfo.Conn = remote
+ connInfo.UnderlyingConn = workConn
if pxy.proxyPlugin != nil {
// if plugin is set, let plugin handle connection first
xl.Debugf("handle by plugin: %s", pxy.proxyPlugin.Name())
- pxy.proxyPlugin.Handle(pxy.ctx, remote, workConn, &extraInfo)
+ pxy.proxyPlugin.Handle(pxy.ctx, &connInfo)
xl.Debugf("handle by plugin finished")
return
}
@@ -210,8 +219,8 @@ func (pxy *BaseProxy) HandleTCPWorkConnection(workConn net.Conn, m *msg.StartWor
xl.Debugf("join connections, localConn(l[%s] r[%s]) workConn(l[%s] r[%s])", localConn.LocalAddr().String(),
localConn.RemoteAddr().String(), workConn.LocalAddr().String(), workConn.RemoteAddr().String())
- if extraInfo.ProxyProtocolHeader != nil {
- if _, err := extraInfo.ProxyProtocolHeader.WriteTo(localConn); err != nil {
+ if connInfo.ProxyProtocolHeader != nil {
+ if _, err := connInfo.ProxyProtocolHeader.WriteTo(localConn); err != nil {
workConn.Close()
xl.Errorf("write proxy protocol header to local conn error: %v", err)
return
diff --git a/client/proxy/proxy_manager.go b/client/proxy/proxy_manager.go
index d42aedc0..ea5cc553 100644
--- a/client/proxy/proxy_manager.go
+++ b/client/proxy/proxy_manager.go
@@ -28,12 +28,14 @@ import (
"github.com/fatedier/frp/pkg/msg"
"github.com/fatedier/frp/pkg/transport"
"github.com/fatedier/frp/pkg/util/xlog"
+ "github.com/fatedier/frp/pkg/vnet"
)
type Manager struct {
proxies map[string]*Wrapper
msgTransporter transport.MessageTransporter
inWorkConnCallback func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) bool
+ vnetController *vnet.Controller
closed bool
mu sync.RWMutex
@@ -47,10 +49,12 @@ func NewManager(
ctx context.Context,
clientCfg *v1.ClientCommonConfig,
msgTransporter transport.MessageTransporter,
+ vnetController *vnet.Controller,
) *Manager {
return &Manager{
proxies: make(map[string]*Wrapper),
msgTransporter: msgTransporter,
+ vnetController: vnetController,
closed: false,
clientCfg: clientCfg,
ctx: ctx,
@@ -159,7 +163,7 @@ func (pm *Manager) UpdateAll(proxyCfgs []v1.ProxyConfigurer) {
for _, cfg := range proxyCfgs {
name := cfg.GetBaseConfig().Name
if _, ok := pm.proxies[name]; !ok {
- pxy := NewWrapper(pm.ctx, cfg, pm.clientCfg, pm.HandleEvent, pm.msgTransporter)
+ pxy := NewWrapper(pm.ctx, cfg, pm.clientCfg, pm.HandleEvent, pm.msgTransporter, pm.vnetController)
if pm.inWorkConnCallback != nil {
pxy.SetInWorkConnCallback(pm.inWorkConnCallback)
}
diff --git a/client/proxy/proxy_wrapper.go b/client/proxy/proxy_wrapper.go
index 95048f29..f3f17e2b 100644
--- a/client/proxy/proxy_wrapper.go
+++ b/client/proxy/proxy_wrapper.go
@@ -31,6 +31,7 @@ import (
"github.com/fatedier/frp/pkg/msg"
"github.com/fatedier/frp/pkg/transport"
"github.com/fatedier/frp/pkg/util/xlog"
+ "github.com/fatedier/frp/pkg/vnet"
)
const (
@@ -73,6 +74,8 @@ type Wrapper struct {
handler event.Handler
msgTransporter transport.MessageTransporter
+ // vnet controller
+ vnetController *vnet.Controller
health uint32
lastSendStartMsg time.Time
@@ -91,6 +94,7 @@ func NewWrapper(
clientCfg *v1.ClientCommonConfig,
eventHandler event.Handler,
msgTransporter transport.MessageTransporter,
+ vnetController *vnet.Controller,
) *Wrapper {
baseInfo := cfg.GetBaseConfig()
xl := xlog.FromContextSafe(ctx).Spawn().AppendPrefix(baseInfo.Name)
@@ -105,6 +109,7 @@ func NewWrapper(
healthNotifyCh: make(chan struct{}),
handler: eventHandler,
msgTransporter: msgTransporter,
+ vnetController: vnetController,
xl: xl,
ctx: xlog.NewContext(ctx, xl),
}
@@ -117,7 +122,7 @@ func NewWrapper(
xl.Tracef("enable health check monitor")
}
- pw.pxy = NewProxy(pw.ctx, pw.Cfg, clientCfg, pw.msgTransporter)
+ pw.pxy = NewProxy(pw.ctx, pw.Cfg, clientCfg, pw.msgTransporter, pw.vnetController)
return pw
}
diff --git a/client/service.go b/client/service.go
index b06706a6..57eb4835 100644
--- a/client/service.go
+++ b/client/service.go
@@ -37,6 +37,7 @@ import (
"github.com/fatedier/frp/pkg/util/version"
"github.com/fatedier/frp/pkg/util/wait"
"github.com/fatedier/frp/pkg/util/xlog"
+ "github.com/fatedier/frp/pkg/vnet"
)
func init() {
@@ -110,6 +111,8 @@ type Service struct {
// web server for admin UI and apis
webServer *httppkg.Server
+ vnetController *vnet.Controller
+
cfgMu sync.RWMutex
common *v1.ClientCommonConfig
proxyCfgs []v1.ProxyConfigurer
@@ -156,6 +159,9 @@ func NewService(options ServiceOptions) (*Service, error) {
if webServer != nil {
webServer.RouteRegister(s.registerRouteHandlers)
}
+ if options.Common.VirtualNet.Address != "" {
+ s.vnetController = vnet.NewController(options.Common.VirtualNet)
+ }
return s, nil
}
@@ -169,6 +175,19 @@ func (svr *Service) Run(ctx context.Context) error {
netpkg.SetDefaultDNSAddress(svr.common.DNSServer)
}
+ if svr.vnetController != nil {
+ if err := svr.vnetController.Init(); err != nil {
+ log.Errorf("init virtual network controller error: %v", err)
+ return err
+ }
+ go func() {
+ log.Infof("virtual network controller start...")
+ if err := svr.vnetController.Run(); err != nil {
+ log.Warnf("virtual network controller exit with error: %v", err)
+ }
+ }()
+ }
+
if svr.webServer != nil {
go func() {
log.Infof("admin server listen on %s", svr.webServer.Address())
@@ -311,12 +330,13 @@ func (svr *Service) loopLoginUntilSuccess(maxInterval time.Duration, firstLoginE
connEncrypted = false
}
sessionCtx := &SessionContext{
- Common: svr.common,
- RunID: svr.runID,
- Conn: conn,
- ConnEncrypted: connEncrypted,
- AuthSetter: svr.authSetter,
- Connector: connector,
+ Common: svr.common,
+ RunID: svr.runID,
+ Conn: conn,
+ ConnEncrypted: connEncrypted,
+ AuthSetter: svr.authSetter,
+ Connector: connector,
+ VnetController: svr.vnetController,
}
ctl, err := NewControl(svr.ctx, sessionCtx)
if err != nil {
diff --git a/client/visitor/stcp.go b/client/visitor/stcp.go
index b26faf52..124202eb 100644
--- a/client/visitor/stcp.go
+++ b/client/visitor/stcp.go
@@ -44,6 +44,10 @@ func (sv *STCPVisitor) Run() (err error) {
}
go sv.internalConnWorker()
+
+ if sv.plugin != nil {
+ sv.plugin.Start()
+ }
return
}
diff --git a/client/visitor/visitor.go b/client/visitor/visitor.go
index d520f735..fb2b3e11 100644
--- a/client/visitor/visitor.go
+++ b/client/visitor/visitor.go
@@ -20,9 +20,11 @@ import (
"sync"
v1 "github.com/fatedier/frp/pkg/config/v1"
+ plugin "github.com/fatedier/frp/pkg/plugin/visitor"
"github.com/fatedier/frp/pkg/transport"
netpkg "github.com/fatedier/frp/pkg/util/net"
"github.com/fatedier/frp/pkg/util/xlog"
+ "github.com/fatedier/frp/pkg/vnet"
)
// Helper wraps some functions for visitor to use.
@@ -34,6 +36,8 @@ type Helper interface {
// MsgTransporter returns the message transporter that is used to send and receive messages
// to the frp server through the controller.
MsgTransporter() transport.MessageTransporter
+ // VNetController returns the vnet controller that is used to manage the virtual network.
+ VNetController() *vnet.Controller
// RunID returns the run id of current controller.
RunID() string
}
@@ -50,14 +54,34 @@ func NewVisitor(
cfg v1.VisitorConfigurer,
clientCfg *v1.ClientCommonConfig,
helper Helper,
-) (visitor Visitor) {
+) (Visitor, error) {
xl := xlog.FromContextSafe(ctx).Spawn().AppendPrefix(cfg.GetBaseConfig().Name)
+ ctx = xlog.NewContext(ctx, xl)
+ var visitor Visitor
baseVisitor := BaseVisitor{
clientCfg: clientCfg,
helper: helper,
- ctx: xlog.NewContext(ctx, xl),
+ ctx: ctx,
internalLn: netpkg.NewInternalListener(),
}
+ if cfg.GetBaseConfig().Plugin.Type != "" {
+ p, err := plugin.Create(
+ cfg.GetBaseConfig().Plugin.Type,
+ plugin.PluginContext{
+ Name: cfg.GetBaseConfig().Name,
+ Ctx: ctx,
+ VnetController: helper.VNetController(),
+ HandleConn: func(conn net.Conn) {
+ _ = baseVisitor.AcceptConn(conn)
+ },
+ },
+ cfg.GetBaseConfig().Plugin.VisitorPluginOptions,
+ )
+ if err != nil {
+ return nil, err
+ }
+ baseVisitor.plugin = p
+ }
switch cfg := cfg.(type) {
case *v1.STCPVisitorConfig:
visitor = &STCPVisitor{
@@ -77,7 +101,7 @@ func NewVisitor(
checkCloseCh: make(chan struct{}),
}
}
- return
+ return visitor, nil
}
type BaseVisitor struct {
@@ -85,6 +109,7 @@ type BaseVisitor struct {
helper Helper
l net.Listener
internalLn *netpkg.InternalListener
+ plugin plugin.Plugin
mu sync.RWMutex
ctx context.Context
@@ -101,4 +126,7 @@ func (v *BaseVisitor) Close() {
if v.internalLn != nil {
v.internalLn.Close()
}
+ if v.plugin != nil {
+ v.plugin.Close()
+ }
}
diff --git a/client/visitor/visitor_manager.go b/client/visitor/visitor_manager.go
index 6ff65dab..b3539c69 100644
--- a/client/visitor/visitor_manager.go
+++ b/client/visitor/visitor_manager.go
@@ -27,6 +27,7 @@ import (
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/transport"
"github.com/fatedier/frp/pkg/util/xlog"
+ "github.com/fatedier/frp/pkg/vnet"
)
type Manager struct {
@@ -50,6 +51,7 @@ func NewManager(
clientCfg *v1.ClientCommonConfig,
connectServer func() (net.Conn, error),
msgTransporter transport.MessageTransporter,
+ vnetController *vnet.Controller,
) *Manager {
m := &Manager{
clientCfg: clientCfg,
@@ -62,6 +64,7 @@ func NewManager(
m.helper = &visitorHelperImpl{
connectServerFn: connectServer,
msgTransporter: msgTransporter,
+ vnetController: vnetController,
transferConnFn: m.TransferConn,
runID: runID,
}
@@ -112,7 +115,11 @@ func (vm *Manager) Close() {
func (vm *Manager) startVisitor(cfg v1.VisitorConfigurer) (err error) {
xl := xlog.FromContextSafe(vm.ctx)
name := cfg.GetBaseConfig().Name
- visitor := NewVisitor(vm.ctx, cfg, vm.clientCfg, vm.helper)
+ visitor, err := NewVisitor(vm.ctx, cfg, vm.clientCfg, vm.helper)
+ if err != nil {
+ xl.Warnf("new visitor error: %v", err)
+ return
+ }
err = visitor.Run()
if err != nil {
xl.Warnf("start error: %v", err)
@@ -187,6 +194,7 @@ func (vm *Manager) TransferConn(name string, conn net.Conn) error {
type visitorHelperImpl struct {
connectServerFn func() (net.Conn, error)
msgTransporter transport.MessageTransporter
+ vnetController *vnet.Controller
transferConnFn func(name string, conn net.Conn) error
runID string
}
@@ -203,6 +211,10 @@ func (v *visitorHelperImpl) MsgTransporter() transport.MessageTransporter {
return v.msgTransporter
}
+func (v *visitorHelperImpl) VNetController() *vnet.Controller {
+ return v.vnetController
+}
+
func (v *visitorHelperImpl) RunID() string {
return v.runID
}
diff --git a/client/visitor/xtcp.go b/client/visitor/xtcp.go
index a1efd72b..51f29ad2 100644
--- a/client/visitor/xtcp.go
+++ b/client/visitor/xtcp.go
@@ -73,6 +73,10 @@ func (sv *XTCPVisitor) Run() (err error) {
sv.retryLimiter = rate.NewLimiter(rate.Every(time.Hour/time.Duration(sv.cfg.MaxRetriesAnHour)), sv.cfg.MaxRetriesAnHour)
go sv.keepTunnelOpenWorker()
}
+
+ if sv.plugin != nil {
+ sv.plugin.Start()
+ }
return
}
@@ -157,9 +161,9 @@ func (sv *XTCPVisitor) keepTunnelOpenWorker() {
func (sv *XTCPVisitor) handleConn(userConn net.Conn) {
xl := xlog.FromContextSafe(sv.ctx)
- isConnTrasfered := false
+ isConnTransfered := false
defer func() {
- if !isConnTrasfered {
+ if !isConnTransfered {
userConn.Close()
}
}()
@@ -187,7 +191,7 @@ func (sv *XTCPVisitor) handleConn(userConn net.Conn) {
xl.Errorf("transfer connection to visitor %s error: %v", sv.cfg.FallbackTo, err)
return
}
- isConnTrasfered = true
+ isConnTransfered = true
return
}
diff --git a/cmd/frpc/sub/root.go b/cmd/frpc/sub/root.go
index b844ddfd..ee89c489 100644
--- a/cmd/frpc/sub/root.go
+++ b/cmd/frpc/sub/root.go
@@ -31,6 +31,7 @@ import (
"github.com/fatedier/frp/pkg/config"
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/config/v1/validation"
+ "github.com/fatedier/frp/pkg/featuregate"
"github.com/fatedier/frp/pkg/util/log"
"github.com/fatedier/frp/pkg/util/version"
)
@@ -120,6 +121,12 @@ func runClient(cfgFilePath string) error {
"please use yaml/json/toml format instead!\n")
}
+ if len(cfg.FeatureGates) > 0 {
+ if err := featuregate.SetFromMap(cfg.FeatureGates); err != nil {
+ return err
+ }
+ }
+
warning, err := validation.ValidateAllClientConfig(cfg, proxyCfgs, visitorCfgs)
if warning != nil {
fmt.Printf("WARNING: %v\n", warning)
diff --git a/conf/frpc_full_example.toml b/conf/frpc_full_example.toml
index eb447392..7d4838cd 100644
--- a/conf/frpc_full_example.toml
+++ b/conf/frpc_full_example.toml
@@ -129,6 +129,15 @@ transport.tls.enable = true
# It affects the udp and sudp proxy.
udpPacketSize = 1500
+# Feature gates allows you to enable or disable experimental features
+# Format is a map of feature names to boolean values
+# You can enable specific features:
+#featureGates = { VirtualNet = true }
+
+# VirtualNet settings for experimental virtual network capabilities
+# The virtual network feature requires enabling the VirtualNet feature gate above
+# virtualNet.address = "100.86.1.1/24"
+
# Additional metadatas for client.
metadatas.var1 = "abc"
metadatas.var2 = "123"
@@ -358,6 +367,13 @@ localPort = 22
# Otherwise, visitors from same user can connect. '*' means allow all users.
allowUsers = ["user1", "user2"]
+[[proxies]]
+name = "vnet-server"
+type = "stcp"
+secretKey = "your-secret-key"
+[proxies.plugin]
+type = "virtual_net"
+
# frpc role visitor -> frps -> frpc role server
[[visitors]]
name = "secret_tcp_visitor"
@@ -389,3 +405,13 @@ maxRetriesAnHour = 8
minRetryInterval = 90
# fallbackTo = "stcp_visitor"
# fallbackTimeoutMs = 500
+
+[[visitors]]
+name = "vnet-visitor"
+type = "stcp"
+serverName = "vnet-server"
+secretKey = "your-secret-key"
+bindPort = -1
+[visitors.plugin]
+type = "virtual_net"
+destinationIP = "100.86.0.1"
diff --git a/doc/virtual_net.md b/doc/virtual_net.md
new file mode 100644
index 00000000..62653e9f
--- /dev/null
+++ b/doc/virtual_net.md
@@ -0,0 +1,75 @@
+# Virtual Network (VirtualNet)
+
+*Alpha feature added in v0.62.0*
+
+The VirtualNet feature enables frp to create and manage virtual network connections between clients and visitors through a TUN interface. This allows for IP-level routing between machines, extending frp beyond simple port forwarding to support full network connectivity.
+
+> **Note**: VirtualNet is an Alpha stage feature and is currently unstable. Its configuration methods and functionality may be adjusted and changed at any time in subsequent versions. Do not use this feature in production environments; it is only recommended for testing and evaluation purposes.
+
+## Enabling VirtualNet
+
+Since VirtualNet is currently an alpha feature, you need to enable it with feature gates in your configuration:
+
+```toml
+# frpc.toml
+featureGates = { VirtualNet = true }
+```
+
+## Basic Configuration
+
+To use the virtual network capabilities:
+
+1. First, configure your frpc with a virtual network address:
+
+```toml
+# frpc.toml
+serverAddr = "x.x.x.x"
+serverPort = 7000
+featureGates = { VirtualNet = true }
+
+# Configure the virtual network interface
+virtualNet.address = "100.86.0.1/24"
+```
+
+2. For client proxies, use the `virtual_net` plugin:
+
+```toml
+# frpc.toml (server side)
+[[proxies]]
+name = "vnet-server"
+type = "stcp"
+secretKey = "your-secret-key"
+[proxies.plugin]
+type = "virtual_net"
+```
+
+3. For visitor connections, configure the `virtual_net` visitor plugin:
+
+```toml
+# frpc.toml (client side)
+serverAddr = "x.x.x.x"
+serverPort = 7000
+featureGates = {
+ VirtualNet = true
+}
+
+# Configure the virtual network interface
+virtualNet.address = "100.86.0.2/24"
+
+[[visitors]]
+name = "vnet-visitor"
+type = "stcp"
+serverName = "vnet-server"
+secretKey = "your-secret-key"
+bindPort = -1
+[visitors.plugin]
+type = "virtual_net"
+destinationIP = "100.86.0.1"
+```
+
+## Requirements and Limitations
+
+- **Permissions**: Creating a TUN interface requires elevated permissions (root/admin)
+- **Platform Support**: Currently supported on Linux and macOS
+- **Default Status**: As an alpha feature, VirtualNet is disabled by default
+- **Configuration**: A valid IP/CIDR must be provided for each endpoint in the virtual network
diff --git a/go.mod b/go.mod
index ecf20211..0b8ee2a5 100644
--- a/go.mod
+++ b/go.mod
@@ -4,7 +4,7 @@ go 1.23.0
require (
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
- github.com/coreos/go-oidc/v3 v3.10.0
+ github.com/coreos/go-oidc/v3 v3.14.1
github.com/fatedier/golib v0.5.1
github.com/google/uuid v1.6.0
github.com/gorilla/mux v1.8.1
@@ -19,16 +19,19 @@ require (
github.com/quic-go/quic-go v0.48.2
github.com/rodaine/table v1.2.0
github.com/samber/lo v1.47.0
+ github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8
github.com/spf13/cobra v1.8.0
github.com/spf13/pflag v1.0.5
- github.com/stretchr/testify v1.9.0
+ github.com/stretchr/testify v1.10.0
github.com/tidwall/gjson v1.17.1
+ github.com/vishvananda/netlink v1.3.0
github.com/xtaci/kcp-go/v5 v5.6.13
- golang.org/x/crypto v0.30.0
- golang.org/x/net v0.32.0
- golang.org/x/oauth2 v0.16.0
- golang.org/x/sync v0.10.0
+ golang.org/x/crypto v0.37.0
+ golang.org/x/net v0.39.0
+ golang.org/x/oauth2 v0.28.0
+ golang.org/x/sync v0.13.0
golang.org/x/time v0.5.0
+ golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
gopkg.in/ini.v1 v1.67.0
k8s.io/apimachinery v0.28.8
k8s.io/client-go v0.28.8
@@ -39,10 +42,9 @@ require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
- github.com/go-jose/go-jose/v4 v4.0.1 // indirect
+ github.com/go-jose/go-jose/v4 v4.0.5 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
- github.com/golang/protobuf v1.5.4 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/pprof v0.0.0-20241206021119-61a79c692802 // indirect
@@ -64,13 +66,14 @@ require (
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
github.com/tjfoc/gmsm v1.4.1 // indirect
+ github.com/vishvananda/netns v0.0.4 // indirect
go.uber.org/mock v0.5.0 // indirect
golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d // indirect
golang.org/x/mod v0.22.0 // indirect
- golang.org/x/sys v0.28.0 // indirect
- golang.org/x/text v0.21.0 // indirect
+ golang.org/x/sys v0.32.0 // indirect
+ golang.org/x/text v0.24.0 // indirect
golang.org/x/tools v0.28.0 // indirect
- google.golang.org/appengine v1.6.8 // indirect
+ golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
google.golang.org/protobuf v1.34.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
diff --git a/go.sum b/go.sum
index e1053561..a39d174e 100644
--- a/go.sum
+++ b/go.sum
@@ -11,8 +11,8 @@ github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
-github.com/coreos/go-oidc/v3 v3.10.0 h1:tDnXHnLyiTVyT/2zLDGj09pFPkhND8Gl8lnTRhoEaJU=
-github.com/coreos/go-oidc/v3 v3.10.0/go.mod h1:5j11xcw0D3+SGxn6Z/WFADsgcWVMyNAlSQupk0KK3ac=
+github.com/coreos/go-oidc/v3 v3.14.1 h1:9ePWwfdwC4QKRlCXsJGou56adA/owXczOzwKdOumLqk=
+github.com/coreos/go-oidc/v3 v3.14.1/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU=
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -25,8 +25,8 @@ github.com/fatedier/golib v0.5.1 h1:hcKAnaw5mdI/1KWRGejxR+i1Hn/NvbY5UsMKDr7o13M=
github.com/fatedier/golib v0.5.1/go.mod h1:W6kIYkIFxHsTzbgqg5piCxIiDo4LzwgTY6R5W8l9NFQ=
github.com/fatedier/yamux v0.0.0-20230628132301-7aca4898904d h1:ynk1ra0RUqDWQfvFi5KtMiSobkVQ3cNc0ODb8CfIETo=
github.com/fatedier/yamux v0.0.0-20230628132301-7aca4898904d/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
-github.com/go-jose/go-jose/v4 v4.0.1 h1:QVEPDE3OluqXBQZDcnNvQrInro2h0e4eqNbnZSWqS6U=
-github.com/go-jose/go-jose/v4 v4.0.1/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY=
+github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
+github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
@@ -42,17 +42,14 @@ github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrU
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
-github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
-github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
-github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
-github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4=
+github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/pprof v0.0.0-20241206021119-61a79c692802 h1:US08AXzP0bLurpzFUV3Poa9ZijrRdd1zAIOVtoHEiS8=
@@ -117,6 +114,8 @@ github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncj
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc=
github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU=
+github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 h1:TG/diQgUe0pntT/2D9tmUCz4VNwm9MfrtPr0SU2qSX8=
+github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8/go.mod h1:P5HUIBuIWKbyjl083/loAegFkfbFNx5i2qEP4CNbm7E=
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
@@ -129,8 +128,9 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
-github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/templexxx/cpu v0.1.1 h1:isxHaxBXpYFWnk2DReuKkigaZyrjs2+9ypIdGP4h+HI=
github.com/templexxx/cpu v0.1.1/go.mod h1:w7Tb+7qgcAlIyX4NhLuDKt78AHA5SzPmq0Wj6HiEnnk=
github.com/templexxx/xorsimd v0.4.3 h1:9AQTFHd7Bhk3dIT7Al2XeBX5DWOvsUPZCuhyAtNbHjU=
@@ -143,6 +143,10 @@ github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE=
+github.com/vishvananda/netlink v1.3.0 h1:X7l42GfcV4S6E4vHTsw48qbrV+9PVojNfIhZcwQdrZk=
+github.com/vishvananda/netlink v1.3.0/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs=
+github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
+github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
github.com/xtaci/kcp-go/v5 v5.6.13 h1:FEjtz9+D4p8t2x4WjciGt/jsIuhlWjjgPCCWjrVR4Hk=
github.com/xtaci/kcp-go/v5 v5.6.13/go.mod h1:75S1AKYYzNUSXIv30h+jPKJYZUwqpfvLshu63nCNSOM=
github.com/xtaci/lossyconn v0.0.0-20200209145036-adba10fffc37 h1:EWU6Pktpas0n8lLQwDsRyZfmkPeRbdgPtW609es+/9E=
@@ -156,8 +160,8 @@ golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
-golang.org/x/crypto v0.30.0 h1:RwoQn3GkWiMkzlX562cLB7OxWvjH1L8xutO2WoJcRoY=
-golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
+golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
+golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d h1:0olWaB5pg3+oychR51GUVCEsGkeCU/2JxjBgIo4f3M0=
golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c=
@@ -181,18 +185,18 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
-golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI=
-golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs=
+golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
+golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
-golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ=
-golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o=
+golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc=
+golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
-golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
+golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -201,29 +205,30 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
-golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
+golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
-golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
-golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
+golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
+golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
-golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
-golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
-golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
+golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
+golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -238,10 +243,12 @@ golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8=
golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
+golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
+golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 h1:/jFs0duh4rdb8uIfPMv78iAJGcPKDeqAFnaLBropIC4=
+golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
-google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
-google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
@@ -254,8 +261,6 @@ google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQ
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
-google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
-google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -268,6 +273,8 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 h1:TbRPT0HtzFP3Cno1zZo7yPzEEnfu8EjLfl6IU9VfqkQ=
+gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259/go.mod h1:AVgIgHMwK63XvmAzWG9vLQ41YnVHN0du0tEC46fI7yY=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
k8s.io/apimachinery v0.28.8 h1:hi/nrxHwk4QLV+W/SHve1bypTE59HCDorLY1stBIxKQ=
diff --git a/pkg/config/v1/client.go b/pkg/config/v1/client.go
index d43ec1bc..d616fc0a 100644
--- a/pkg/config/v1/client.go
+++ b/pkg/config/v1/client.go
@@ -58,9 +58,14 @@ type ClientCommonConfig struct {
// set.
Start []string `json:"start,omitempty"`
- Log LogConfig `json:"log,omitempty"`
- WebServer WebServerConfig `json:"webServer,omitempty"`
- Transport ClientTransportConfig `json:"transport,omitempty"`
+ Log LogConfig `json:"log,omitempty"`
+ WebServer WebServerConfig `json:"webServer,omitempty"`
+ Transport ClientTransportConfig `json:"transport,omitempty"`
+ VirtualNet VirtualNetConfig `json:"virtualNet,omitempty"`
+
+ // FeatureGates specifies a set of feature gates to enable or disable.
+ // This can be used to enable alpha/beta features or disable default features.
+ FeatureGates map[string]bool `json:"featureGates,omitempty"`
// UDPPacketSize specifies the udp packet size
// By default, this value is 1500
@@ -204,3 +209,7 @@ type AuthOIDCClientConfig struct {
// this field will be transfer to map[string][]string in OIDC token generator.
AdditionalEndpointParams map[string]string `json:"additionalEndpointParams,omitempty"`
}
+
+type VirtualNetConfig struct {
+ Address string `json:"address,omitempty"`
+}
diff --git a/pkg/config/v1/plugin.go b/pkg/config/v1/proxy_plugin.go
similarity index 96%
rename from pkg/config/v1/plugin.go
rename to pkg/config/v1/proxy_plugin.go
index cdf3cf26..128ccae6 100644
--- a/pkg/config/v1/plugin.go
+++ b/pkg/config/v1/proxy_plugin.go
@@ -26,6 +26,32 @@ import (
"github.com/fatedier/frp/pkg/util/util"
)
+const (
+ PluginHTTP2HTTPS = "http2https"
+ PluginHTTPProxy = "http_proxy"
+ PluginHTTPS2HTTP = "https2http"
+ PluginHTTPS2HTTPS = "https2https"
+ PluginHTTP2HTTP = "http2http"
+ PluginSocks5 = "socks5"
+ PluginStaticFile = "static_file"
+ PluginUnixDomainSocket = "unix_domain_socket"
+ PluginTLS2Raw = "tls2raw"
+ PluginVirtualNet = "virtual_net"
+)
+
+var clientPluginOptionsTypeMap = map[string]reflect.Type{
+ PluginHTTP2HTTPS: reflect.TypeOf(HTTP2HTTPSPluginOptions{}),
+ PluginHTTPProxy: reflect.TypeOf(HTTPProxyPluginOptions{}),
+ PluginHTTPS2HTTP: reflect.TypeOf(HTTPS2HTTPPluginOptions{}),
+ PluginHTTPS2HTTPS: reflect.TypeOf(HTTPS2HTTPSPluginOptions{}),
+ PluginHTTP2HTTP: reflect.TypeOf(HTTP2HTTPPluginOptions{}),
+ PluginSocks5: reflect.TypeOf(Socks5PluginOptions{}),
+ PluginStaticFile: reflect.TypeOf(StaticFilePluginOptions{}),
+ PluginUnixDomainSocket: reflect.TypeOf(UnixDomainSocketPluginOptions{}),
+ PluginTLS2Raw: reflect.TypeOf(TLS2RawPluginOptions{}),
+ PluginVirtualNet: reflect.TypeOf(VirtualNetPluginOptions{}),
+}
+
type ClientPluginOptions interface {
Complete()
}
@@ -74,30 +100,6 @@ func (c *TypedClientPluginOptions) MarshalJSON() ([]byte, error) {
return json.Marshal(c.ClientPluginOptions)
}
-const (
- PluginHTTP2HTTPS = "http2https"
- PluginHTTPProxy = "http_proxy"
- PluginHTTPS2HTTP = "https2http"
- PluginHTTPS2HTTPS = "https2https"
- PluginHTTP2HTTP = "http2http"
- PluginSocks5 = "socks5"
- PluginStaticFile = "static_file"
- PluginUnixDomainSocket = "unix_domain_socket"
- PluginTLS2Raw = "tls2raw"
-)
-
-var clientPluginOptionsTypeMap = map[string]reflect.Type{
- PluginHTTP2HTTPS: reflect.TypeOf(HTTP2HTTPSPluginOptions{}),
- PluginHTTPProxy: reflect.TypeOf(HTTPProxyPluginOptions{}),
- PluginHTTPS2HTTP: reflect.TypeOf(HTTPS2HTTPPluginOptions{}),
- PluginHTTPS2HTTPS: reflect.TypeOf(HTTPS2HTTPSPluginOptions{}),
- PluginHTTP2HTTP: reflect.TypeOf(HTTP2HTTPPluginOptions{}),
- PluginSocks5: reflect.TypeOf(Socks5PluginOptions{}),
- PluginStaticFile: reflect.TypeOf(StaticFilePluginOptions{}),
- PluginUnixDomainSocket: reflect.TypeOf(UnixDomainSocketPluginOptions{}),
- PluginTLS2Raw: reflect.TypeOf(TLS2RawPluginOptions{}),
-}
-
type HTTP2HTTPSPluginOptions struct {
Type string `json:"type,omitempty"`
LocalAddr string `json:"localAddr,omitempty"`
@@ -185,3 +187,9 @@ type TLS2RawPluginOptions struct {
}
func (o *TLS2RawPluginOptions) Complete() {}
+
+type VirtualNetPluginOptions struct {
+ Type string `json:"type,omitempty"`
+}
+
+func (o *VirtualNetPluginOptions) Complete() {}
diff --git a/pkg/config/v1/validation/client.go b/pkg/config/v1/validation/client.go
index cc46607c..bae21fda 100644
--- a/pkg/config/v1/validation/client.go
+++ b/pkg/config/v1/validation/client.go
@@ -23,6 +23,7 @@ import (
"github.com/samber/lo"
v1 "github.com/fatedier/frp/pkg/config/v1"
+ "github.com/fatedier/frp/pkg/featuregate"
)
func ValidateClientCommonConfig(c *v1.ClientCommonConfig) (Warning, error) {
@@ -30,6 +31,13 @@ func ValidateClientCommonConfig(c *v1.ClientCommonConfig) (Warning, error) {
warnings Warning
errs error
)
+ // validate feature gates
+ if c.VirtualNet.Address != "" {
+ if !featuregate.Enabled(featuregate.VirtualNet) {
+ return warnings, fmt.Errorf("VirtualNet feature is not enabled; enable it by setting the appropriate feature gate flag")
+ }
+ }
+
if !slices.Contains(SupportedAuthMethods, c.Auth.Method) {
errs = AppendError(errs, fmt.Errorf("invalid auth method, optional values are %v", SupportedAuthMethods))
}
diff --git a/pkg/config/v1/visitor.go b/pkg/config/v1/visitor.go
index 51fe88a6..f00391c3 100644
--- a/pkg/config/v1/visitor.go
+++ b/pkg/config/v1/visitor.go
@@ -44,6 +44,9 @@ type VisitorBaseConfig struct {
// It can be less than 0, it means don't bind to the port and only receive connections redirected from
// other visitors. (This is not supported for SUDP now)
BindPort int `json:"bindPort,omitempty"`
+
+ // Plugin specifies what plugin should be used.
+ Plugin TypedVisitorPluginOptions `json:"plugin,omitempty"`
}
func (c *VisitorBaseConfig) GetBaseConfig() *VisitorBaseConfig {
diff --git a/pkg/config/v1/visitor_plugin.go b/pkg/config/v1/visitor_plugin.go
new file mode 100644
index 00000000..5a4909bd
--- /dev/null
+++ b/pkg/config/v1/visitor_plugin.go
@@ -0,0 +1,86 @@
+// Copyright 2025 The frp Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package v1
+
+import (
+ "bytes"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "reflect"
+)
+
+const (
+ VisitorPluginVirtualNet = "virtual_net"
+)
+
+var visitorPluginOptionsTypeMap = map[string]reflect.Type{
+ VisitorPluginVirtualNet: reflect.TypeOf(VirtualNetVisitorPluginOptions{}),
+}
+
+type VisitorPluginOptions interface {
+ Complete()
+}
+
+type TypedVisitorPluginOptions struct {
+ Type string `json:"type"`
+ VisitorPluginOptions
+}
+
+func (c *TypedVisitorPluginOptions) UnmarshalJSON(b []byte) error {
+ if len(b) == 4 && string(b) == "null" {
+ return nil
+ }
+
+ typeStruct := struct {
+ Type string `json:"type"`
+ }{}
+ if err := json.Unmarshal(b, &typeStruct); err != nil {
+ return err
+ }
+
+ c.Type = typeStruct.Type
+ if c.Type == "" {
+ return errors.New("visitor plugin type is empty")
+ }
+
+ v, ok := visitorPluginOptionsTypeMap[typeStruct.Type]
+ if !ok {
+ return fmt.Errorf("unknown visitor plugin type: %s", typeStruct.Type)
+ }
+ options := reflect.New(v).Interface().(VisitorPluginOptions)
+
+ decoder := json.NewDecoder(bytes.NewBuffer(b))
+ if DisallowUnknownFields {
+ decoder.DisallowUnknownFields()
+ }
+
+ if err := decoder.Decode(options); err != nil {
+ return fmt.Errorf("unmarshal VisitorPluginOptions error: %v", err)
+ }
+ c.VisitorPluginOptions = options
+ return nil
+}
+
+func (c *TypedVisitorPluginOptions) MarshalJSON() ([]byte, error) {
+ return json.Marshal(c.VisitorPluginOptions)
+}
+
+type VirtualNetVisitorPluginOptions struct {
+ Type string `json:"type"`
+ DestinationIP string `json:"destinationIP"`
+}
+
+func (o *VirtualNetVisitorPluginOptions) Complete() {}
diff --git a/pkg/featuregate/feature_gate.go b/pkg/featuregate/feature_gate.go
new file mode 100644
index 00000000..c5fd684b
--- /dev/null
+++ b/pkg/featuregate/feature_gate.go
@@ -0,0 +1,219 @@
+// Copyright 2025 The frp Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package featuregate
+
+import (
+ "fmt"
+ "sort"
+ "strings"
+ "sync"
+ "sync/atomic"
+)
+
+// Feature represents a feature gate name
+type Feature string
+
+// FeatureStage represents the maturity level of a feature
+type FeatureStage string
+
+const (
+ // Alpha means the feature is experimental and disabled by default
+ Alpha FeatureStage = "ALPHA"
+ // Beta means the feature is more stable but still might change and is disabled by default
+ Beta FeatureStage = "BETA"
+ // GA means the feature is generally available and enabled by default
+ GA FeatureStage = ""
+)
+
+// FeatureSpec describes a feature and its properties
+type FeatureSpec struct {
+ // Default is the default enablement state for the feature
+ Default bool
+ // LockToDefault indicates the feature cannot be changed from its default
+ LockToDefault bool
+ // Stage indicates the maturity level of the feature
+ Stage FeatureStage
+}
+
+// Define all available features here
+var (
+ VirtualNet = Feature("VirtualNet")
+)
+
+// defaultFeatures defines default features with their specifications
+var defaultFeatures = map[Feature]FeatureSpec{
+ // Actual features
+ VirtualNet: {Default: false, Stage: Alpha},
+}
+
+// FeatureGate indicates whether a given feature is enabled or not
+type FeatureGate interface {
+ // Enabled returns true if the key is enabled
+ Enabled(key Feature) bool
+ // KnownFeatures returns a slice of strings describing the known features
+ KnownFeatures() []string
+}
+
+// MutableFeatureGate allows for dynamic feature gate configuration
+type MutableFeatureGate interface {
+ FeatureGate
+
+ // SetFromMap sets feature gate values from a map[string]bool
+ SetFromMap(m map[string]bool) error
+ // Add adds features to the feature gate
+ Add(features map[Feature]FeatureSpec) error
+ // String returns a string representing the feature gate configuration
+ String() string
+}
+
+// featureGate implements the FeatureGate and MutableFeatureGate interfaces
+type featureGate struct {
+ // lock guards writes to known, enabled, and reads/writes of closed
+ lock sync.Mutex
+ // known holds a map[Feature]FeatureSpec
+ known atomic.Value
+ // enabled holds a map[Feature]bool
+ enabled atomic.Value
+ // closed is set to true once the feature gates are considered immutable
+ closed bool
+}
+
+// NewFeatureGate creates a new feature gate with the default features
+func NewFeatureGate() MutableFeatureGate {
+ known := map[Feature]FeatureSpec{}
+ for k, v := range defaultFeatures {
+ known[k] = v
+ }
+
+ f := &featureGate{}
+ f.known.Store(known)
+ f.enabled.Store(map[Feature]bool{})
+ return f
+}
+
+// SetFromMap sets feature gate values from a map[string]bool
+func (f *featureGate) SetFromMap(m map[string]bool) error {
+ f.lock.Lock()
+ defer f.lock.Unlock()
+
+ // Copy existing state
+ known := map[Feature]FeatureSpec{}
+ for k, v := range f.known.Load().(map[Feature]FeatureSpec) {
+ known[k] = v
+ }
+ enabled := map[Feature]bool{}
+ for k, v := range f.enabled.Load().(map[Feature]bool) {
+ enabled[k] = v
+ }
+
+ // Apply the new settings
+ for k, v := range m {
+ k := Feature(k)
+ featureSpec, ok := known[k]
+ if !ok {
+ return fmt.Errorf("unrecognized feature gate: %s", k)
+ }
+ if featureSpec.LockToDefault && featureSpec.Default != v {
+ return fmt.Errorf("cannot set feature gate %v to %v, feature is locked to %v", k, v, featureSpec.Default)
+ }
+ enabled[k] = v
+ }
+
+ // Persist the changes
+ f.known.Store(known)
+ f.enabled.Store(enabled)
+ return nil
+}
+
+// Add adds features to the feature gate
+func (f *featureGate) Add(features map[Feature]FeatureSpec) error {
+ f.lock.Lock()
+ defer f.lock.Unlock()
+
+ if f.closed {
+ return fmt.Errorf("cannot add feature gates after the feature gate is closed")
+ }
+
+ // Copy existing state
+ known := map[Feature]FeatureSpec{}
+ for k, v := range f.known.Load().(map[Feature]FeatureSpec) {
+ known[k] = v
+ }
+
+ // Add new features
+ for name, spec := range features {
+ if existingSpec, found := known[name]; found {
+ if existingSpec == spec {
+ continue
+ }
+ return fmt.Errorf("feature gate %q with different spec already exists: %v", name, existingSpec)
+ }
+ known[name] = spec
+ }
+
+ // Persist changes
+ f.known.Store(known)
+
+ return nil
+}
+
+// String returns a string containing all enabled feature gates, formatted as "key1=value1,key2=value2,..."
+func (f *featureGate) String() string {
+ pairs := []string{}
+ for k, v := range f.enabled.Load().(map[Feature]bool) {
+ pairs = append(pairs, fmt.Sprintf("%s=%t", k, v))
+ }
+ sort.Strings(pairs)
+ return strings.Join(pairs, ",")
+}
+
+// Enabled returns true if the key is enabled
+func (f *featureGate) Enabled(key Feature) bool {
+ if v, ok := f.enabled.Load().(map[Feature]bool)[key]; ok {
+ return v
+ }
+ if v, ok := f.known.Load().(map[Feature]FeatureSpec)[key]; ok {
+ return v.Default
+ }
+ return false
+}
+
+// KnownFeatures returns a slice of strings describing the FeatureGate's known features
+// GA features are hidden from the list
+func (f *featureGate) KnownFeatures() []string {
+ knownFeatures := f.known.Load().(map[Feature]FeatureSpec)
+ known := make([]string, 0, len(knownFeatures))
+ for k, v := range knownFeatures {
+ if v.Stage == GA {
+ continue
+ }
+ known = append(known, fmt.Sprintf("%s=true|false (%s - default=%t)", k, v.Stage, v.Default))
+ }
+ sort.Strings(known)
+ return known
+}
+
+// Default feature gates instance
+var DefaultFeatureGates = NewFeatureGate()
+
+// Enabled checks if a feature is enabled in the default feature gates
+func Enabled(name Feature) bool {
+ return DefaultFeatureGates.Enabled(name)
+}
+
+// SetFromMap sets feature gate values from a map in the default feature gates
+func SetFromMap(featureMap map[string]bool) error {
+ return DefaultFeatureGates.SetFromMap(featureMap)
+}
diff --git a/pkg/plugin/client/http2http.go b/pkg/plugin/client/http2http.go
index d7c4c7e4..889a10f6 100644
--- a/pkg/plugin/client/http2http.go
+++ b/pkg/plugin/client/http2http.go
@@ -14,13 +14,11 @@
//go:build !frps
-package plugin
+package client
import (
"context"
- "io"
stdlog "log"
- "net"
"net/http"
"net/http/httputil"
@@ -42,7 +40,7 @@ type HTTP2HTTPPlugin struct {
s *http.Server
}
-func NewHTTP2HTTPPlugin(options v1.ClientPluginOptions) (Plugin, error) {
+func NewHTTP2HTTPPlugin(_ PluginContext, options v1.ClientPluginOptions) (Plugin, error) {
opts := options.(*v1.HTTP2HTTPPluginOptions)
listener := NewProxyListener()
@@ -80,8 +78,8 @@ func NewHTTP2HTTPPlugin(options v1.ClientPluginOptions) (Plugin, error) {
return p, nil
}
-func (p *HTTP2HTTPPlugin) Handle(_ context.Context, conn io.ReadWriteCloser, realConn net.Conn, _ *ExtraInfo) {
- wrapConn := netpkg.WrapReadWriteCloserToConn(conn, realConn)
+func (p *HTTP2HTTPPlugin) Handle(_ context.Context, connInfo *ConnectionInfo) {
+ wrapConn := netpkg.WrapReadWriteCloserToConn(connInfo.Conn, connInfo.UnderlyingConn)
_ = p.l.PutConn(wrapConn)
}
diff --git a/pkg/plugin/client/http2https.go b/pkg/plugin/client/http2https.go
index 66f90989..538f2850 100644
--- a/pkg/plugin/client/http2https.go
+++ b/pkg/plugin/client/http2https.go
@@ -14,14 +14,12 @@
//go:build !frps
-package plugin
+package client
import (
"context"
"crypto/tls"
- "io"
stdlog "log"
- "net"
"net/http"
"net/http/httputil"
@@ -43,7 +41,7 @@ type HTTP2HTTPSPlugin struct {
s *http.Server
}
-func NewHTTP2HTTPSPlugin(options v1.ClientPluginOptions) (Plugin, error) {
+func NewHTTP2HTTPSPlugin(_ PluginContext, options v1.ClientPluginOptions) (Plugin, error) {
opts := options.(*v1.HTTP2HTTPSPluginOptions)
listener := NewProxyListener()
@@ -89,8 +87,8 @@ func NewHTTP2HTTPSPlugin(options v1.ClientPluginOptions) (Plugin, error) {
return p, nil
}
-func (p *HTTP2HTTPSPlugin) Handle(_ context.Context, conn io.ReadWriteCloser, realConn net.Conn, _ *ExtraInfo) {
- wrapConn := netpkg.WrapReadWriteCloserToConn(conn, realConn)
+func (p *HTTP2HTTPSPlugin) Handle(_ context.Context, connInfo *ConnectionInfo) {
+ wrapConn := netpkg.WrapReadWriteCloserToConn(connInfo.Conn, connInfo.UnderlyingConn)
_ = p.l.PutConn(wrapConn)
}
diff --git a/pkg/plugin/client/http_proxy.go b/pkg/plugin/client/http_proxy.go
index b7491bd1..0f6b55f4 100644
--- a/pkg/plugin/client/http_proxy.go
+++ b/pkg/plugin/client/http_proxy.go
@@ -14,7 +14,7 @@
//go:build !frps
-package plugin
+package client
import (
"bufio"
@@ -45,7 +45,7 @@ type HTTPProxy struct {
s *http.Server
}
-func NewHTTPProxyPlugin(options v1.ClientPluginOptions) (Plugin, error) {
+func NewHTTPProxyPlugin(_ PluginContext, options v1.ClientPluginOptions) (Plugin, error) {
opts := options.(*v1.HTTPProxyPluginOptions)
listener := NewProxyListener()
@@ -69,8 +69,8 @@ func (hp *HTTPProxy) Name() string {
return v1.PluginHTTPProxy
}
-func (hp *HTTPProxy) Handle(_ context.Context, conn io.ReadWriteCloser, realConn net.Conn, _ *ExtraInfo) {
- wrapConn := netpkg.WrapReadWriteCloserToConn(conn, realConn)
+func (hp *HTTPProxy) Handle(_ context.Context, connInfo *ConnectionInfo) {
+ wrapConn := netpkg.WrapReadWriteCloserToConn(connInfo.Conn, connInfo.UnderlyingConn)
sc, rd := libnet.NewSharedConn(wrapConn)
firstBytes := make([]byte, 7)
diff --git a/pkg/plugin/client/https2http.go b/pkg/plugin/client/https2http.go
index 9632a6fb..963b9d2e 100644
--- a/pkg/plugin/client/https2http.go
+++ b/pkg/plugin/client/https2http.go
@@ -14,15 +14,13 @@
//go:build !frps
-package plugin
+package client
import (
"context"
"crypto/tls"
"fmt"
- "io"
stdlog "log"
- "net"
"net/http"
"net/http/httputil"
"time"
@@ -48,7 +46,7 @@ type HTTPS2HTTPPlugin struct {
s *http.Server
}
-func NewHTTPS2HTTPPlugin(options v1.ClientPluginOptions) (Plugin, error) {
+func NewHTTPS2HTTPPlugin(_ PluginContext, options v1.ClientPluginOptions) (Plugin, error) {
opts := options.(*v1.HTTPS2HTTPPluginOptions)
listener := NewProxyListener()
@@ -106,10 +104,10 @@ func NewHTTPS2HTTPPlugin(options v1.ClientPluginOptions) (Plugin, error) {
return p, nil
}
-func (p *HTTPS2HTTPPlugin) Handle(_ context.Context, conn io.ReadWriteCloser, realConn net.Conn, extra *ExtraInfo) {
- wrapConn := netpkg.WrapReadWriteCloserToConn(conn, realConn)
- if extra.SrcAddr != nil {
- wrapConn.SetRemoteAddr(extra.SrcAddr)
+func (p *HTTPS2HTTPPlugin) Handle(_ context.Context, connInfo *ConnectionInfo) {
+ wrapConn := netpkg.WrapReadWriteCloserToConn(connInfo.Conn, connInfo.UnderlyingConn)
+ if connInfo.SrcAddr != nil {
+ wrapConn.SetRemoteAddr(connInfo.SrcAddr)
}
_ = p.l.PutConn(wrapConn)
}
diff --git a/pkg/plugin/client/https2https.go b/pkg/plugin/client/https2https.go
index 8121e094..5c669d36 100644
--- a/pkg/plugin/client/https2https.go
+++ b/pkg/plugin/client/https2https.go
@@ -14,15 +14,13 @@
//go:build !frps
-package plugin
+package client
import (
"context"
"crypto/tls"
"fmt"
- "io"
stdlog "log"
- "net"
"net/http"
"net/http/httputil"
"time"
@@ -48,7 +46,7 @@ type HTTPS2HTTPSPlugin struct {
s *http.Server
}
-func NewHTTPS2HTTPSPlugin(options v1.ClientPluginOptions) (Plugin, error) {
+func NewHTTPS2HTTPSPlugin(_ PluginContext, options v1.ClientPluginOptions) (Plugin, error) {
opts := options.(*v1.HTTPS2HTTPSPluginOptions)
listener := NewProxyListener()
@@ -112,10 +110,10 @@ func NewHTTPS2HTTPSPlugin(options v1.ClientPluginOptions) (Plugin, error) {
return p, nil
}
-func (p *HTTPS2HTTPSPlugin) Handle(_ context.Context, conn io.ReadWriteCloser, realConn net.Conn, extra *ExtraInfo) {
- wrapConn := netpkg.WrapReadWriteCloserToConn(conn, realConn)
- if extra.SrcAddr != nil {
- wrapConn.SetRemoteAddr(extra.SrcAddr)
+func (p *HTTPS2HTTPSPlugin) Handle(_ context.Context, connInfo *ConnectionInfo) {
+ wrapConn := netpkg.WrapReadWriteCloserToConn(connInfo.Conn, connInfo.UnderlyingConn)
+ if connInfo.SrcAddr != nil {
+ wrapConn.SetRemoteAddr(connInfo.SrcAddr)
}
_ = p.l.PutConn(wrapConn)
}
diff --git a/pkg/plugin/client/plugin.go b/pkg/plugin/client/plugin.go
index 3dce8592..7bcd0489 100644
--- a/pkg/plugin/client/plugin.go
+++ b/pkg/plugin/client/plugin.go
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package plugin
+package client
import (
"context"
@@ -25,13 +25,18 @@ import (
pp "github.com/pires/go-proxyproto"
v1 "github.com/fatedier/frp/pkg/config/v1"
+ "github.com/fatedier/frp/pkg/vnet"
)
+type PluginContext struct {
+ Name string
+ VnetController *vnet.Controller
+}
+
// Creators is used for create plugins to handle connections.
var creators = make(map[string]CreatorFn)
-// params has prefix "plugin_"
-type CreatorFn func(options v1.ClientPluginOptions) (Plugin, error)
+type CreatorFn func(pluginCtx PluginContext, options v1.ClientPluginOptions) (Plugin, error)
func Register(name string, fn CreatorFn) {
if _, exist := creators[name]; exist {
@@ -40,16 +45,19 @@ func Register(name string, fn CreatorFn) {
creators[name] = fn
}
-func Create(name string, options v1.ClientPluginOptions) (p Plugin, err error) {
- if fn, ok := creators[name]; ok {
- p, err = fn(options)
+func Create(pluginName string, pluginCtx PluginContext, options v1.ClientPluginOptions) (p Plugin, err error) {
+ if fn, ok := creators[pluginName]; ok {
+ p, err = fn(pluginCtx, options)
} else {
- err = fmt.Errorf("plugin [%s] is not registered", name)
+ err = fmt.Errorf("plugin [%s] is not registered", pluginName)
}
return
}
-type ExtraInfo struct {
+type ConnectionInfo struct {
+ Conn io.ReadWriteCloser
+ UnderlyingConn net.Conn
+
ProxyProtocolHeader *pp.Header
SrcAddr net.Addr
DstAddr net.Addr
@@ -58,7 +66,7 @@ type ExtraInfo struct {
type Plugin interface {
Name() string
- Handle(ctx context.Context, conn io.ReadWriteCloser, realConn net.Conn, extra *ExtraInfo)
+ Handle(ctx context.Context, connInfo *ConnectionInfo)
Close() error
}
diff --git a/pkg/plugin/client/socks5.go b/pkg/plugin/client/socks5.go
index 7478ebe1..752dd1e1 100644
--- a/pkg/plugin/client/socks5.go
+++ b/pkg/plugin/client/socks5.go
@@ -14,13 +14,12 @@
//go:build !frps
-package plugin
+package client
import (
"context"
"io"
"log"
- "net"
gosocks5 "github.com/armon/go-socks5"
@@ -36,7 +35,7 @@ type Socks5Plugin struct {
Server *gosocks5.Server
}
-func NewSocks5Plugin(options v1.ClientPluginOptions) (p Plugin, err error) {
+func NewSocks5Plugin(_ PluginContext, options v1.ClientPluginOptions) (p Plugin, err error) {
opts := options.(*v1.Socks5PluginOptions)
cfg := &gosocks5.Config{
@@ -51,9 +50,9 @@ func NewSocks5Plugin(options v1.ClientPluginOptions) (p Plugin, err error) {
return
}
-func (sp *Socks5Plugin) Handle(_ context.Context, conn io.ReadWriteCloser, realConn net.Conn, _ *ExtraInfo) {
- defer conn.Close()
- wrapConn := netpkg.WrapReadWriteCloserToConn(conn, realConn)
+func (sp *Socks5Plugin) Handle(_ context.Context, connInfo *ConnectionInfo) {
+ defer connInfo.Conn.Close()
+ wrapConn := netpkg.WrapReadWriteCloserToConn(connInfo.Conn, connInfo.UnderlyingConn)
_ = sp.Server.ServeConn(wrapConn)
}
diff --git a/pkg/plugin/client/static_file.go b/pkg/plugin/client/static_file.go
index 02cb9930..0bab120a 100644
--- a/pkg/plugin/client/static_file.go
+++ b/pkg/plugin/client/static_file.go
@@ -14,12 +14,10 @@
//go:build !frps
-package plugin
+package client
import (
"context"
- "io"
- "net"
"net/http"
"time"
@@ -40,7 +38,7 @@ type StaticFilePlugin struct {
s *http.Server
}
-func NewStaticFilePlugin(options v1.ClientPluginOptions) (Plugin, error) {
+func NewStaticFilePlugin(_ PluginContext, options v1.ClientPluginOptions) (Plugin, error) {
opts := options.(*v1.StaticFilePluginOptions)
listener := NewProxyListener()
@@ -70,8 +68,8 @@ func NewStaticFilePlugin(options v1.ClientPluginOptions) (Plugin, error) {
return sp, nil
}
-func (sp *StaticFilePlugin) Handle(_ context.Context, conn io.ReadWriteCloser, realConn net.Conn, _ *ExtraInfo) {
- wrapConn := netpkg.WrapReadWriteCloserToConn(conn, realConn)
+func (sp *StaticFilePlugin) Handle(_ context.Context, connInfo *ConnectionInfo) {
+ wrapConn := netpkg.WrapReadWriteCloserToConn(connInfo.Conn, connInfo.UnderlyingConn)
_ = sp.l.PutConn(wrapConn)
}
diff --git a/pkg/plugin/client/tls2raw.go b/pkg/plugin/client/tls2raw.go
index adcc7741..445b6c91 100644
--- a/pkg/plugin/client/tls2raw.go
+++ b/pkg/plugin/client/tls2raw.go
@@ -14,12 +14,11 @@
//go:build !frps
-package plugin
+package client
import (
"context"
"crypto/tls"
- "io"
"net"
libio "github.com/fatedier/golib/io"
@@ -40,7 +39,7 @@ type TLS2RawPlugin struct {
tlsConfig *tls.Config
}
-func NewTLS2RawPlugin(options v1.ClientPluginOptions) (Plugin, error) {
+func NewTLS2RawPlugin(_ PluginContext, options v1.ClientPluginOptions) (Plugin, error) {
opts := options.(*v1.TLS2RawPluginOptions)
p := &TLS2RawPlugin{
@@ -55,10 +54,10 @@ func NewTLS2RawPlugin(options v1.ClientPluginOptions) (Plugin, error) {
return p, nil
}
-func (p *TLS2RawPlugin) Handle(ctx context.Context, conn io.ReadWriteCloser, realConn net.Conn, _ *ExtraInfo) {
+func (p *TLS2RawPlugin) Handle(ctx context.Context, connInfo *ConnectionInfo) {
xl := xlog.FromContextSafe(ctx)
- wrapConn := netpkg.WrapReadWriteCloserToConn(conn, realConn)
+ wrapConn := netpkg.WrapReadWriteCloserToConn(connInfo.Conn, connInfo.UnderlyingConn)
tlsConn := tls.Server(wrapConn, p.tlsConfig)
if err := tlsConn.Handshake(); err != nil {
diff --git a/pkg/plugin/client/unix_domain_socket.go b/pkg/plugin/client/unix_domain_socket.go
index b6aa6075..52d9c652 100644
--- a/pkg/plugin/client/unix_domain_socket.go
+++ b/pkg/plugin/client/unix_domain_socket.go
@@ -14,11 +14,10 @@
//go:build !frps
-package plugin
+package client
import (
"context"
- "io"
"net"
libio "github.com/fatedier/golib/io"
@@ -35,7 +34,7 @@ type UnixDomainSocketPlugin struct {
UnixAddr *net.UnixAddr
}
-func NewUnixDomainSocketPlugin(options v1.ClientPluginOptions) (p Plugin, err error) {
+func NewUnixDomainSocketPlugin(_ PluginContext, options v1.ClientPluginOptions) (p Plugin, err error) {
opts := options.(*v1.UnixDomainSocketPluginOptions)
unixAddr, errRet := net.ResolveUnixAddr("unix", opts.UnixPath)
@@ -50,20 +49,20 @@ func NewUnixDomainSocketPlugin(options v1.ClientPluginOptions) (p Plugin, err er
return
}
-func (uds *UnixDomainSocketPlugin) Handle(ctx context.Context, conn io.ReadWriteCloser, _ net.Conn, extra *ExtraInfo) {
+func (uds *UnixDomainSocketPlugin) Handle(ctx context.Context, connInfo *ConnectionInfo) {
xl := xlog.FromContextSafe(ctx)
localConn, err := net.DialUnix("unix", nil, uds.UnixAddr)
if err != nil {
xl.Warnf("dial to uds %s error: %v", uds.UnixAddr, err)
return
}
- if extra.ProxyProtocolHeader != nil {
- if _, err := extra.ProxyProtocolHeader.WriteTo(localConn); err != nil {
+ if connInfo.ProxyProtocolHeader != nil {
+ if _, err := connInfo.ProxyProtocolHeader.WriteTo(localConn); err != nil {
return
}
}
- libio.Join(localConn, conn)
+ libio.Join(localConn, connInfo.Conn)
}
func (uds *UnixDomainSocketPlugin) Name() string {
diff --git a/pkg/plugin/client/virtual_net.go b/pkg/plugin/client/virtual_net.go
new file mode 100644
index 00000000..e1b29fdc
--- /dev/null
+++ b/pkg/plugin/client/virtual_net.go
@@ -0,0 +1,71 @@
+// Copyright 2025 The frp Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//go:build !frps
+
+package client
+
+import (
+ "context"
+
+ v1 "github.com/fatedier/frp/pkg/config/v1"
+ "github.com/fatedier/frp/pkg/util/xlog"
+)
+
+func init() {
+ Register(v1.PluginVirtualNet, NewVirtualNetPlugin)
+}
+
+type VirtualNetPlugin struct {
+ pluginCtx PluginContext
+ opts *v1.VirtualNetPluginOptions
+}
+
+func NewVirtualNetPlugin(pluginCtx PluginContext, options v1.ClientPluginOptions) (Plugin, error) {
+ opts := options.(*v1.VirtualNetPluginOptions)
+
+ p := &VirtualNetPlugin{
+ pluginCtx: pluginCtx,
+ opts: opts,
+ }
+ return p, nil
+}
+
+func (p *VirtualNetPlugin) Handle(ctx context.Context, connInfo *ConnectionInfo) {
+ xl := xlog.FromContextSafe(ctx)
+
+ // Verify if virtual network controller is available
+ if p.pluginCtx.VnetController == nil {
+ return
+ }
+
+ // Register the connection with the controller
+ routeName := p.pluginCtx.Name
+ err := p.pluginCtx.VnetController.RegisterServerConn(ctx, routeName, connInfo.Conn)
+ if err != nil {
+ xl.Errorf("virtual net failed to register server connection: %v", err)
+ return
+ }
+}
+
+func (p *VirtualNetPlugin) Name() string {
+ return v1.PluginVirtualNet
+}
+
+func (p *VirtualNetPlugin) Close() error {
+ if p.pluginCtx.VnetController != nil {
+ p.pluginCtx.VnetController.UnregisterServerConn(p.pluginCtx.Name)
+ }
+ return nil
+}
diff --git a/pkg/plugin/server/http.go b/pkg/plugin/server/http.go
index 6046c38a..196993ef 100644
--- a/pkg/plugin/server/http.go
+++ b/pkg/plugin/server/http.go
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package plugin
+package server
import (
"bytes"
diff --git a/pkg/plugin/server/manager.go b/pkg/plugin/server/manager.go
index 8f23b529..dabfb46c 100644
--- a/pkg/plugin/server/manager.go
+++ b/pkg/plugin/server/manager.go
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package plugin
+package server
import (
"context"
diff --git a/pkg/plugin/server/plugin.go b/pkg/plugin/server/plugin.go
index 9456ee9b..3d3c8cfd 100644
--- a/pkg/plugin/server/plugin.go
+++ b/pkg/plugin/server/plugin.go
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package plugin
+package server
import (
"context"
diff --git a/pkg/plugin/server/tracer.go b/pkg/plugin/server/tracer.go
index 2f4f2ccc..5b6ede18 100644
--- a/pkg/plugin/server/tracer.go
+++ b/pkg/plugin/server/tracer.go
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package plugin
+package server
import (
"context"
diff --git a/pkg/plugin/server/types.go b/pkg/plugin/server/types.go
index c9fc4a40..4a5b7527 100644
--- a/pkg/plugin/server/types.go
+++ b/pkg/plugin/server/types.go
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package plugin
+package server
import (
"github.com/fatedier/frp/pkg/msg"
diff --git a/pkg/plugin/visitor/plugin.go b/pkg/plugin/visitor/plugin.go
new file mode 100644
index 00000000..94adce09
--- /dev/null
+++ b/pkg/plugin/visitor/plugin.go
@@ -0,0 +1,58 @@
+// Copyright 2025 The frp Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package visitor
+
+import (
+ "context"
+ "fmt"
+ "net"
+
+ v1 "github.com/fatedier/frp/pkg/config/v1"
+ "github.com/fatedier/frp/pkg/vnet"
+)
+
+type PluginContext struct {
+ Name string
+ Ctx context.Context
+ VnetController *vnet.Controller
+ HandleConn func(net.Conn)
+}
+
+// Creators is used for create plugins to handle connections.
+var creators = make(map[string]CreatorFn)
+
+type CreatorFn func(pluginCtx PluginContext, options v1.VisitorPluginOptions) (Plugin, error)
+
+func Register(name string, fn CreatorFn) {
+ if _, exist := creators[name]; exist {
+ panic(fmt.Sprintf("plugin [%s] is already registered", name))
+ }
+ creators[name] = fn
+}
+
+func Create(pluginName string, pluginCtx PluginContext, options v1.VisitorPluginOptions) (p Plugin, err error) {
+ if fn, ok := creators[pluginName]; ok {
+ p, err = fn(pluginCtx, options)
+ } else {
+ err = fmt.Errorf("plugin [%s] is not registered", pluginName)
+ }
+ return
+}
+
+type Plugin interface {
+ Name() string
+ Start()
+ Close() error
+}
diff --git a/pkg/plugin/visitor/virtual_net.go b/pkg/plugin/visitor/virtual_net.go
new file mode 100644
index 00000000..e452e14f
--- /dev/null
+++ b/pkg/plugin/visitor/virtual_net.go
@@ -0,0 +1,232 @@
+// Copyright 2025 The frp Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//go:build !frps
+
+package visitor
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "net"
+ "sync"
+ "time"
+
+ v1 "github.com/fatedier/frp/pkg/config/v1"
+ netutil "github.com/fatedier/frp/pkg/util/net"
+ "github.com/fatedier/frp/pkg/util/xlog"
+)
+
+func init() {
+ Register(v1.VisitorPluginVirtualNet, NewVirtualNetPlugin)
+}
+
+type VirtualNetPlugin struct {
+ pluginCtx PluginContext
+
+ routes []net.IPNet
+
+ mu sync.Mutex
+ controllerConn net.Conn
+ closeSignal chan struct{}
+
+ ctx context.Context
+ cancel context.CancelFunc
+}
+
+func NewVirtualNetPlugin(pluginCtx PluginContext, options v1.VisitorPluginOptions) (Plugin, error) {
+ opts := options.(*v1.VirtualNetVisitorPluginOptions)
+
+ p := &VirtualNetPlugin{
+ pluginCtx: pluginCtx,
+ routes: make([]net.IPNet, 0),
+ }
+
+ p.ctx, p.cancel = context.WithCancel(pluginCtx.Ctx)
+
+ if opts.DestinationIP == "" {
+ return nil, errors.New("destinationIP is required")
+ }
+
+ // Parse DestinationIP as a single IP and create a host route
+ ip := net.ParseIP(opts.DestinationIP)
+ if ip == nil {
+ return nil, fmt.Errorf("invalid destination IP address [%s]", opts.DestinationIP)
+ }
+
+ var mask net.IPMask
+ if ip.To4() != nil {
+ mask = net.CIDRMask(32, 32) // /32 for IPv4
+ } else {
+ mask = net.CIDRMask(128, 128) // /128 for IPv6
+ }
+ p.routes = append(p.routes, net.IPNet{IP: ip, Mask: mask})
+
+ return p, nil
+}
+
+func (p *VirtualNetPlugin) Name() string {
+ return v1.VisitorPluginVirtualNet
+}
+
+func (p *VirtualNetPlugin) Start() {
+ xl := xlog.FromContextSafe(p.pluginCtx.Ctx)
+ if p.pluginCtx.VnetController == nil {
+ return
+ }
+
+ routeStr := "unknown"
+ if len(p.routes) > 0 {
+ routeStr = p.routes[0].String()
+ }
+ xl.Infof("Starting VirtualNetPlugin for visitor [%s], attempting to register routes for %s", p.pluginCtx.Name, routeStr)
+
+ go p.run()
+}
+
+func (p *VirtualNetPlugin) run() {
+ xl := xlog.FromContextSafe(p.ctx)
+ reconnectDelay := 10 * time.Second
+
+ for {
+ // Create a signal channel for this connection attempt
+ currentCloseSignal := make(chan struct{})
+
+ // Store the signal channel under lock
+ p.mu.Lock()
+ p.closeSignal = currentCloseSignal
+ p.mu.Unlock()
+
+ select {
+ case <-p.ctx.Done():
+ xl.Infof("VirtualNetPlugin run loop for visitor [%s] stopping (context cancelled before pipe creation).", p.pluginCtx.Name)
+ // Ensure controllerConn from previous loop is cleaned up if necessary
+ p.cleanupControllerConn(xl)
+ return
+ default:
+ }
+
+ controllerConn, pluginConn := net.Pipe()
+
+ // Store controllerConn under lock for cleanup purposes
+ p.mu.Lock()
+ p.controllerConn = controllerConn
+ p.mu.Unlock()
+
+ // Wrap pluginConn using CloseNotifyConn
+ pluginNotifyConn := netutil.WrapCloseNotifyConn(pluginConn, func() {
+ close(currentCloseSignal) // Signal the run loop
+ })
+
+ xl.Infof("Attempting to register client route for visitor [%s]", p.pluginCtx.Name)
+ err := p.pluginCtx.VnetController.RegisterClientRoute(p.ctx, p.pluginCtx.Name, p.routes, controllerConn)
+ if err != nil {
+ xl.Errorf("Failed to register client route for visitor [%s]: %v. Retrying after %v", p.pluginCtx.Name, err, reconnectDelay)
+ p.cleanupPipePair(xl, controllerConn, pluginConn) // Close both ends on registration failure
+
+ // Wait before retrying registration, unless context is cancelled
+ select {
+ case <-time.After(reconnectDelay):
+ continue // Retry the loop
+ case <-p.ctx.Done():
+ xl.Infof("VirtualNetPlugin registration retry wait interrupted for visitor [%s]", p.pluginCtx.Name)
+ return // Exit loop if context is cancelled during wait
+ }
+ }
+
+ xl.Infof("Successfully registered client route for visitor [%s]. Starting connection handler with CloseNotifyConn.", p.pluginCtx.Name)
+
+ // Pass the CloseNotifyConn to HandleConn.
+ // HandleConn is responsible for calling Close() on pluginNotifyConn.
+ p.pluginCtx.HandleConn(pluginNotifyConn)
+
+ // Wait for either the plugin context to be cancelled or the wrapper's Close() to be called via the signal channel.
+ select {
+ case <-p.ctx.Done():
+ xl.Infof("VirtualNetPlugin run loop stopping for visitor [%s] (context cancelled while waiting).", p.pluginCtx.Name)
+ // Context cancelled, ensure controller side is closed if HandleConn didn't close its side yet.
+ p.cleanupControllerConn(xl)
+ return
+ case <-currentCloseSignal:
+ xl.Infof("Detected connection closed via CloseNotifyConn for visitor [%s].", p.pluginCtx.Name)
+ // HandleConn closed the plugin side (pluginNotifyConn). The closeFn was called, closing currentCloseSignal.
+ // We still need to close the controller side.
+ p.cleanupControllerConn(xl)
+
+ // Add a delay before attempting to reconnect, respecting context cancellation.
+ xl.Infof("Waiting %v before attempting reconnection for visitor [%s]...", reconnectDelay, p.pluginCtx.Name)
+ select {
+ case <-time.After(reconnectDelay):
+ // Delay completed, loop will continue.
+ case <-p.ctx.Done():
+ xl.Infof("VirtualNetPlugin reconnection delay interrupted for visitor [%s]", p.pluginCtx.Name)
+ return // Exit loop if context is cancelled during wait
+ }
+ // Loop will continue to reconnect.
+ }
+
+ // Loop will restart, context check at the beginning of the loop is sufficient.
+ xl.Infof("Re-establishing virtual connection for visitor [%s]...", p.pluginCtx.Name)
+ }
+}
+
+// cleanupControllerConn closes the current controllerConn (if it exists) under lock.
+func (p *VirtualNetPlugin) cleanupControllerConn(xl *xlog.Logger) {
+ p.mu.Lock()
+ defer p.mu.Unlock()
+ if p.controllerConn != nil {
+ xl.Debugf("Cleaning up controllerConn for visitor [%s]", p.pluginCtx.Name)
+ p.controllerConn.Close()
+ p.controllerConn = nil
+ }
+ // Also clear the closeSignal reference for the completed/cancelled connection attempt
+ p.closeSignal = nil
+}
+
+// cleanupPipePair closes both ends of a pipe, used typically when registration fails.
+func (p *VirtualNetPlugin) cleanupPipePair(xl *xlog.Logger, controllerConn, pluginConn net.Conn) {
+ xl.Debugf("Cleaning up pipe pair for visitor [%s] after registration failure", p.pluginCtx.Name)
+ controllerConn.Close()
+ pluginConn.Close()
+ p.mu.Lock()
+ p.controllerConn = nil // Ensure field is nil if it was briefly set
+ p.closeSignal = nil // Ensure field is nil if it was briefly set
+ p.mu.Unlock()
+}
+
+// Close initiates the plugin shutdown.
+func (p *VirtualNetPlugin) Close() error {
+ xl := xlog.FromContextSafe(p.pluginCtx.Ctx) // Use base context for close logging
+ xl.Infof("Closing VirtualNetPlugin for visitor [%s]", p.pluginCtx.Name)
+
+ // 1. Signal the run loop goroutine to stop via context cancellation.
+ p.cancel()
+
+ // 2. Unregister the route from the controller.
+ // This might implicitly cause the VnetController to close its end of the pipe (controllerConn).
+ if p.pluginCtx.VnetController != nil {
+ p.pluginCtx.VnetController.UnregisterClientRoute(p.pluginCtx.Name)
+ xl.Infof("Unregistered client route for visitor [%s]", p.pluginCtx.Name)
+ } else {
+ xl.Warnf("VnetController is nil during close for visitor [%s], cannot unregister route", p.pluginCtx.Name)
+ }
+
+ // 3. Explicitly close the controller side of the pipe managed by this plugin.
+ // This ensures the pipe is broken even if the run loop is stuck or HandleConn hasn't closed its end.
+ p.cleanupControllerConn(xl)
+ xl.Infof("Finished cleaning up connections during close for visitor [%s]", p.pluginCtx.Name)
+
+ return nil
+}
diff --git a/pkg/util/version/version.go b/pkg/util/version/version.go
index f56e1ec0..e74233c7 100644
--- a/pkg/util/version/version.go
+++ b/pkg/util/version/version.go
@@ -14,7 +14,7 @@
package version
-var version = "0.61.2"
+var version = "0.62.0"
func Full() string {
return version
diff --git a/pkg/vnet/controller.go b/pkg/vnet/controller.go
new file mode 100644
index 00000000..d43147a3
--- /dev/null
+++ b/pkg/vnet/controller.go
@@ -0,0 +1,360 @@
+// Copyright 2025 The frp Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package vnet
+
+import (
+ "context"
+ "encoding/base64"
+ "fmt"
+ "io"
+ "net"
+ "sync"
+
+ "github.com/fatedier/golib/pool"
+ "github.com/songgao/water/waterutil"
+ "golang.org/x/net/ipv4"
+ "golang.org/x/net/ipv6"
+
+ v1 "github.com/fatedier/frp/pkg/config/v1"
+ "github.com/fatedier/frp/pkg/util/log"
+ "github.com/fatedier/frp/pkg/util/xlog"
+)
+
+const (
+ maxPacketSize = 1420
+)
+
+type Controller struct {
+ addr string
+
+ tun io.ReadWriteCloser
+ clientRouter *clientRouter // Route based on destination IP (client mode)
+ serverRouter *serverRouter // Route based on source IP (server mode)
+}
+
+func NewController(cfg v1.VirtualNetConfig) *Controller {
+ return &Controller{
+ addr: cfg.Address,
+ clientRouter: newClientRouter(),
+ serverRouter: newServerRouter(),
+ }
+}
+
+func (c *Controller) Init() error {
+ tunDevice, err := OpenTun(context.Background(), c.addr)
+ if err != nil {
+ return err
+ }
+ c.tun = tunDevice
+ return nil
+}
+
+func (c *Controller) Run() error {
+ conn := c.tun
+
+ for {
+ buf := pool.GetBuf(maxPacketSize)
+ n, err := conn.Read(buf)
+ if err != nil {
+ pool.PutBuf(buf)
+ log.Warnf("vnet read from tun error: %v", err)
+ return err
+ }
+
+ c.handlePacket(buf[:n])
+ pool.PutBuf(buf)
+ }
+}
+
+// handlePacket processes a single packet. The caller is responsible for managing the buffer.
+func (c *Controller) handlePacket(buf []byte) {
+ log.Tracef("vnet read from tun [%d]: %s", len(buf), base64.StdEncoding.EncodeToString(buf))
+
+ var src, dst net.IP
+ switch {
+ case waterutil.IsIPv4(buf):
+ header, err := ipv4.ParseHeader(buf)
+ if err != nil {
+ log.Warnf("parse ipv4 header error:", err)
+ return
+ }
+ src = header.Src
+ dst = header.Dst
+ log.Tracef("%s >> %s %d/%-4d %-4x %d",
+ header.Src, header.Dst,
+ header.Len, header.TotalLen, header.ID, header.Flags)
+ case waterutil.IsIPv6(buf):
+ header, err := ipv6.ParseHeader(buf)
+ if err != nil {
+ log.Warnf("parse ipv6 header error:", err)
+ return
+ }
+ src = header.Src
+ dst = header.Dst
+ log.Tracef("%s >> %s %d %d",
+ header.Src, header.Dst,
+ header.PayloadLen, header.TrafficClass)
+ default:
+ log.Tracef("unknown packet, discarded(%d)", len(buf))
+ return
+ }
+
+ targetConn, err := c.clientRouter.findConn(dst)
+ if err == nil {
+ if err := WriteMessage(targetConn, buf); err != nil {
+ log.Warnf("write to client target conn error: %v", err)
+ }
+ return
+ }
+
+ targetConn, err = c.serverRouter.findConnBySrc(dst)
+ if err == nil {
+ if err := WriteMessage(targetConn, buf); err != nil {
+ log.Warnf("write to server target conn error: %v", err)
+ }
+ return
+ }
+
+ log.Tracef("no route found for packet from %s to %s", src, dst)
+}
+
+func (c *Controller) Stop() error {
+ return c.tun.Close()
+}
+
+// Client connection read loop
+func (c *Controller) readLoopClient(ctx context.Context, conn io.ReadWriteCloser) {
+ xl := xlog.FromContextSafe(ctx)
+ for {
+ data, err := ReadMessage(conn)
+ if err != nil {
+ xl.Warnf("client read error: %v", err)
+ return
+ }
+
+ if len(data) == 0 {
+ continue
+ }
+
+ switch {
+ case waterutil.IsIPv4(data):
+ header, err := ipv4.ParseHeader(data)
+ if err != nil {
+ xl.Warnf("parse ipv4 header error: %v", err)
+ continue
+ }
+ xl.Tracef("%s >> %s %d/%-4d %-4x %d",
+ header.Src, header.Dst,
+ header.Len, header.TotalLen, header.ID, header.Flags)
+ case waterutil.IsIPv6(data):
+ header, err := ipv6.ParseHeader(data)
+ if err != nil {
+ xl.Warnf("parse ipv6 header error: %v", err)
+ continue
+ }
+ xl.Tracef("%s >> %s %d %d",
+ header.Src, header.Dst,
+ header.PayloadLen, header.TrafficClass)
+ default:
+ xl.Tracef("unknown packet, discarded(%d)", len(data))
+ continue
+ }
+
+ xl.Tracef("vnet write to tun (client) [%d]: %s", len(data), base64.StdEncoding.EncodeToString(data))
+ _, err = c.tun.Write(data)
+ if err != nil {
+ xl.Warnf("client write tun error: %v", err)
+ }
+ }
+}
+
+// Server connection read loop
+func (c *Controller) readLoopServer(ctx context.Context, conn io.ReadWriteCloser) {
+ xl := xlog.FromContextSafe(ctx)
+ for {
+ data, err := ReadMessage(conn)
+ if err != nil {
+ xl.Warnf("server read error: %v", err)
+ return
+ }
+
+ if len(data) == 0 {
+ continue
+ }
+
+ // Register source IP to connection mapping
+ if waterutil.IsIPv4(data) || waterutil.IsIPv6(data) {
+ var src net.IP
+ if waterutil.IsIPv4(data) {
+ header, err := ipv4.ParseHeader(data)
+ if err == nil {
+ src = header.Src
+ c.serverRouter.registerSrcIP(src, conn)
+ }
+ } else {
+ header, err := ipv6.ParseHeader(data)
+ if err == nil {
+ src = header.Src
+ c.serverRouter.registerSrcIP(src, conn)
+ }
+ }
+ }
+
+ xl.Tracef("vnet write to tun (server) [%d]: %s", len(data), base64.StdEncoding.EncodeToString(data))
+ _, err = c.tun.Write(data)
+ if err != nil {
+ xl.Warnf("server write tun error: %v", err)
+ }
+ }
+}
+
+// RegisterClientRoute Register client route (based on destination IP CIDR)
+func (c *Controller) RegisterClientRoute(ctx context.Context, name string, routes []net.IPNet, conn io.ReadWriteCloser) error {
+ if err := c.clientRouter.addRoute(name, routes, conn); err != nil {
+ return err
+ }
+ go c.readLoopClient(ctx, conn)
+ return nil
+}
+
+// RegisterServerConn Register server connection (dynamically associates with source IPs)
+func (c *Controller) RegisterServerConn(ctx context.Context, name string, conn io.ReadWriteCloser) error {
+ if err := c.serverRouter.addConn(name, conn); err != nil {
+ return err
+ }
+ go c.readLoopServer(ctx, conn)
+ return nil
+}
+
+// UnregisterServerConn Remove server connection from routing table
+func (c *Controller) UnregisterServerConn(name string) {
+ c.serverRouter.delConn(name)
+}
+
+// UnregisterClientRoute Remove client route from routing table
+func (c *Controller) UnregisterClientRoute(name string) {
+ c.clientRouter.delRoute(name)
+}
+
+// ParseRoutes Convert route strings to IPNet objects
+func ParseRoutes(routeStrings []string) ([]net.IPNet, error) {
+ routes := make([]net.IPNet, 0, len(routeStrings))
+ for _, r := range routeStrings {
+ _, ipNet, err := net.ParseCIDR(r)
+ if err != nil {
+ return nil, fmt.Errorf("parse route %s error: %v", r, err)
+ }
+ routes = append(routes, *ipNet)
+ }
+ return routes, nil
+}
+
+// Client router (based on destination IP routing)
+type clientRouter struct {
+ routes map[string]*routeElement
+ mu sync.RWMutex
+}
+
+func newClientRouter() *clientRouter {
+ return &clientRouter{
+ routes: make(map[string]*routeElement),
+ }
+}
+
+func (r *clientRouter) addRoute(name string, routes []net.IPNet, conn io.ReadWriteCloser) error {
+ r.mu.Lock()
+ defer r.mu.Unlock()
+ r.routes[name] = &routeElement{
+ name: name,
+ routes: routes,
+ conn: conn,
+ }
+ return nil
+}
+
+func (r *clientRouter) findConn(dst net.IP) (io.Writer, error) {
+ r.mu.RLock()
+ defer r.mu.RUnlock()
+ for _, re := range r.routes {
+ for _, route := range re.routes {
+ if route.Contains(dst) {
+ return re.conn, nil
+ }
+ }
+ }
+ return nil, fmt.Errorf("no route found for destination %s", dst)
+}
+
+func (r *clientRouter) delRoute(name string) {
+ r.mu.Lock()
+ defer r.mu.Unlock()
+ delete(r.routes, name)
+}
+
+// Server router (based on source IP routing)
+type serverRouter struct {
+ namedConns map[string]io.ReadWriteCloser // Name to connection mapping
+ srcIPConns map[string]io.Writer // Source IP string to connection mapping
+ mu sync.RWMutex
+}
+
+func newServerRouter() *serverRouter {
+ return &serverRouter{
+ namedConns: make(map[string]io.ReadWriteCloser),
+ srcIPConns: make(map[string]io.Writer),
+ }
+}
+
+func (r *serverRouter) addConn(name string, conn io.ReadWriteCloser) error {
+ r.mu.Lock()
+ original, ok := r.namedConns[name]
+ r.namedConns[name] = conn
+ r.mu.Unlock()
+ if ok {
+ // Close the original connection if it exists
+ _ = original.Close()
+ }
+ return nil
+}
+
+func (r *serverRouter) findConnBySrc(src net.IP) (io.Writer, error) {
+ r.mu.RLock()
+ defer r.mu.RUnlock()
+ conn, exists := r.srcIPConns[src.String()]
+ if !exists {
+ return nil, fmt.Errorf("no route found for source %s", src)
+ }
+ return conn, nil
+}
+
+func (r *serverRouter) registerSrcIP(src net.IP, conn io.Writer) {
+ r.mu.Lock()
+ defer r.mu.Unlock()
+ r.srcIPConns[src.String()] = conn
+}
+
+func (r *serverRouter) delConn(name string) {
+ r.mu.Lock()
+ defer r.mu.Unlock()
+ delete(r.namedConns, name)
+ // Note: We don't delete mappings from srcIPConns because we don't know which source IPs are associated with this connection
+ // This might cause dangling references, but they will be overwritten on new connections or restart
+}
+
+type routeElement struct {
+ name string
+ routes []net.IPNet
+ conn io.ReadWriteCloser
+}
diff --git a/pkg/vnet/message.go b/pkg/vnet/message.go
new file mode 100644
index 00000000..002b090a
--- /dev/null
+++ b/pkg/vnet/message.go
@@ -0,0 +1,81 @@
+// Copyright 2025 The frp Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package vnet
+
+import (
+ "encoding/binary"
+ "fmt"
+ "io"
+)
+
+// Maximum message size
+const (
+ maxMessageSize = 1024 * 1024 // 1MB
+)
+
+// Format: [length(4 bytes)][data(length bytes)]
+
+// ReadMessage reads a framed message from the reader
+func ReadMessage(r io.Reader) ([]byte, error) {
+ // Read length (4 bytes)
+ var length uint32
+ err := binary.Read(r, binary.LittleEndian, &length)
+ if err != nil {
+ return nil, fmt.Errorf("read message length error: %v", err)
+ }
+
+ // Check length to prevent DoS
+ if length == 0 {
+ return nil, fmt.Errorf("message length is 0")
+ }
+ if length > maxMessageSize {
+ return nil, fmt.Errorf("message too large: %d > %d", length, maxMessageSize)
+ }
+
+ // Read message data
+ data := make([]byte, length)
+ _, err = io.ReadFull(r, data)
+ if err != nil {
+ return nil, fmt.Errorf("read message data error: %v", err)
+ }
+
+ return data, nil
+}
+
+// WriteMessage writes a framed message to the writer
+func WriteMessage(w io.Writer, data []byte) error {
+ // Get data length
+ length := uint32(len(data))
+ if length == 0 {
+ return fmt.Errorf("message data length is 0")
+ }
+ if length > maxMessageSize {
+ return fmt.Errorf("message too large: %d > %d", length, maxMessageSize)
+ }
+
+ // Write length
+ err := binary.Write(w, binary.LittleEndian, length)
+ if err != nil {
+ return fmt.Errorf("write message length error: %v", err)
+ }
+
+ // Write message data
+ _, err = w.Write(data)
+ if err != nil {
+ return fmt.Errorf("write message data error: %v", err)
+ }
+
+ return nil
+}
diff --git a/pkg/vnet/tun.go b/pkg/vnet/tun.go
new file mode 100644
index 00000000..bafc6392
--- /dev/null
+++ b/pkg/vnet/tun.go
@@ -0,0 +1,77 @@
+// Copyright 2025 The frp Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package vnet
+
+import (
+ "context"
+ "io"
+
+ "github.com/fatedier/golib/pool"
+ "golang.zx2c4.com/wireguard/tun"
+)
+
+const (
+ offset = 16
+)
+
+type TunDevice interface {
+ io.ReadWriteCloser
+}
+
+func OpenTun(ctx context.Context, addr string) (TunDevice, error) {
+ td, err := openTun(ctx, addr)
+ if err != nil {
+ return nil, err
+ }
+ return &tunDeviceWrapper{dev: td}, nil
+}
+
+type tunDeviceWrapper struct {
+ dev tun.Device
+}
+
+func (d *tunDeviceWrapper) Read(p []byte) (int, error) {
+ buf := pool.GetBuf(len(p) + offset)
+ defer pool.PutBuf(buf)
+
+ sz := make([]int, 1)
+
+ n, err := d.dev.Read([][]byte{buf}, sz, offset)
+ if err != nil {
+ return 0, err
+ }
+ if n == 0 {
+ return 0, io.EOF
+ }
+
+ dataSize := sz[0]
+ if dataSize > len(p) {
+ dataSize = len(p)
+ }
+ copy(p, buf[offset:offset+dataSize])
+ return dataSize, nil
+}
+
+func (d *tunDeviceWrapper) Write(p []byte) (int, error) {
+ buf := pool.GetBuf(len(p) + offset)
+ defer pool.PutBuf(buf)
+
+ copy(buf[offset:], p)
+ return d.dev.Write([][]byte{buf}, offset)
+}
+
+func (d *tunDeviceWrapper) Close() error {
+ return d.dev.Close()
+}
diff --git a/pkg/vnet/tun_darwin.go b/pkg/vnet/tun_darwin.go
new file mode 100644
index 00000000..9fa7d42c
--- /dev/null
+++ b/pkg/vnet/tun_darwin.go
@@ -0,0 +1,85 @@
+// Copyright 2025 The frp Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package vnet
+
+import (
+ "context"
+ "fmt"
+ "net"
+ "os/exec"
+
+ "golang.zx2c4.com/wireguard/tun"
+)
+
+const (
+ defaultTunName = "utun"
+ defaultMTU = 1420
+)
+
+func openTun(_ context.Context, addr string) (tun.Device, error) {
+ dev, err := tun.CreateTUN(defaultTunName, defaultMTU)
+ if err != nil {
+ return nil, err
+ }
+
+ name, err := dev.Name()
+ if err != nil {
+ return nil, err
+ }
+
+ ip, ipNet, err := net.ParseCIDR(addr)
+ if err != nil {
+ return nil, err
+ }
+
+ // Calculate a peer IP for the point-to-point tunnel
+ peerIP := generatePeerIP(ip)
+
+ // Configure the interface with proper point-to-point addressing
+ if err = exec.Command("ifconfig", name, "inet", ip.String(), peerIP.String(), "mtu", fmt.Sprint(defaultMTU), "up").Run(); err != nil {
+ return nil, err
+ }
+
+ // Add default route for the tunnel subnet
+ routes := []net.IPNet{*ipNet}
+ if err = addRoutes(name, routes); err != nil {
+ return nil, err
+ }
+ return dev, nil
+}
+
+// generatePeerIP creates a peer IP for the point-to-point tunnel
+// by incrementing the last octet of the IP
+func generatePeerIP(ip net.IP) net.IP {
+ // Make a copy to avoid modifying the original
+ peerIP := make(net.IP, len(ip))
+ copy(peerIP, ip)
+
+ // Increment the last octet
+ peerIP[len(peerIP)-1]++
+
+ return peerIP
+}
+
+// addRoutes configures system routes for the TUN interface
+func addRoutes(ifName string, routes []net.IPNet) error {
+ for _, route := range routes {
+ routeStr := route.String()
+ if err := exec.Command("route", "add", "-net", routeStr, "-interface", ifName).Run(); err != nil {
+ return err
+ }
+ }
+ return nil
+}
diff --git a/pkg/vnet/tun_linux.go b/pkg/vnet/tun_linux.go
new file mode 100644
index 00000000..7c9c684c
--- /dev/null
+++ b/pkg/vnet/tun_linux.go
@@ -0,0 +1,84 @@
+// Copyright 2025 The frp Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package vnet
+
+import (
+ "context"
+ "fmt"
+ "net"
+
+ "github.com/vishvananda/netlink"
+ "golang.zx2c4.com/wireguard/tun"
+)
+
+const (
+ defaultTunName = "utun"
+ defaultMTU = 1420
+)
+
+func openTun(_ context.Context, addr string) (tun.Device, error) {
+ dev, err := tun.CreateTUN(defaultTunName, defaultMTU)
+ if err != nil {
+ return nil, err
+ }
+
+ name, err := dev.Name()
+ if err != nil {
+ return nil, err
+ }
+
+ ifn, err := net.InterfaceByName(name)
+ if err != nil {
+ return nil, err
+ }
+
+ link, err := netlink.LinkByName(name)
+ if err != nil {
+ return nil, err
+ }
+
+ ip, cidr, err := net.ParseCIDR(addr)
+ if err != nil {
+ return nil, err
+ }
+ if err := netlink.AddrAdd(link, &netlink.Addr{
+ IPNet: &net.IPNet{
+ IP: ip,
+ Mask: cidr.Mask,
+ },
+ }); err != nil {
+ return nil, err
+ }
+
+ if err := netlink.LinkSetUp(link); err != nil {
+ return nil, err
+ }
+
+ if err = addRoutes(ifn, cidr); err != nil {
+ return nil, err
+ }
+ return dev, nil
+}
+
+func addRoutes(ifn *net.Interface, cidr *net.IPNet) error {
+ r := netlink.Route{
+ Dst: cidr,
+ LinkIndex: ifn.Index,
+ }
+ if err := netlink.RouteReplace(&r); err != nil {
+ return fmt.Errorf("add route to %v error: %v", r.Dst, err)
+ }
+ return nil
+}
diff --git a/pkg/vnet/tun_unsupported.go b/pkg/vnet/tun_unsupported.go
new file mode 100644
index 00000000..0e2a8114
--- /dev/null
+++ b/pkg/vnet/tun_unsupported.go
@@ -0,0 +1,27 @@
+// Copyright 2025 The frp Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//go:build !darwin && !linux
+
+package vnet
+
+import (
+ "context"
+ "fmt"
+ "runtime"
+)
+
+func openTun(ctx context.Context) (TunDevice, error) {
+ return nil, fmt.Errorf("virtual net is not supported on this platform (%s/%s)", runtime.GOOS, runtime.GOARCH)
+}
From e208043323d0ffb5d495c32e14787f509572174c Mon Sep 17 00:00:00 2001
From: fatedier
Date: Wed, 16 Apr 2025 16:17:26 +0800
Subject: [PATCH 2/2] vnet: update tun_unsupported function (#4752)
---
pkg/vnet/tun_unsupported.go | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/pkg/vnet/tun_unsupported.go b/pkg/vnet/tun_unsupported.go
index 0e2a8114..731a06cd 100644
--- a/pkg/vnet/tun_unsupported.go
+++ b/pkg/vnet/tun_unsupported.go
@@ -20,8 +20,10 @@ import (
"context"
"fmt"
"runtime"
+
+ "golang.zx2c4.com/wireguard/tun"
)
-func openTun(ctx context.Context) (TunDevice, error) {
+func openTun(_ context.Context, _ string) (tun.Device, error) {
return nil, fmt.Errorf("virtual net is not supported on this platform (%s/%s)", runtime.GOOS, runtime.GOARCH)
}