virtual-net: initial

This commit is contained in:
fatedier 2025-04-03 17:40:21 +08:00
parent 773169e0c4
commit 22b852a935
35 changed files with 1612 additions and 109 deletions

View File

@ -29,6 +29,7 @@ import (
netpkg "github.com/fatedier/frp/pkg/util/net" netpkg "github.com/fatedier/frp/pkg/util/net"
"github.com/fatedier/frp/pkg/util/wait" "github.com/fatedier/frp/pkg/util/wait"
"github.com/fatedier/frp/pkg/util/xlog" "github.com/fatedier/frp/pkg/util/xlog"
"github.com/fatedier/frp/pkg/vnet"
) )
type SessionContext struct { type SessionContext struct {
@ -46,6 +47,8 @@ type SessionContext struct {
AuthSetter auth.Setter AuthSetter auth.Setter
// Connector is used to create new connections, which could be real TCP connections or virtual streams. // Connector is used to create new connections, which could be real TCP connections or virtual streams.
Connector Connector Connector Connector
// Virtual net controller
VnetController *vnet.Controller
} }
type Control struct { type Control struct {
@ -99,8 +102,9 @@ func NewControl(ctx context.Context, sessionCtx *SessionContext) (*Control, erro
ctl.registerMsgHandlers() ctl.registerMsgHandlers()
ctl.msgTransporter = transport.NewMessageTransporter(ctl.msgDispatcher.SendChannel()) ctl.msgTransporter = transport.NewMessageTransporter(ctl.msgDispatcher.SendChannel())
ctl.pm = proxy.NewManager(ctl.ctx, sessionCtx.Common, 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) ctl.vm = visitor.NewManager(ctl.ctx, sessionCtx.RunID, sessionCtx.Common,
ctl.connectServer, ctl.msgTransporter, sessionCtx.VnetController)
return ctl, nil return ctl, nil
} }

View File

@ -36,6 +36,7 @@ import (
"github.com/fatedier/frp/pkg/transport" "github.com/fatedier/frp/pkg/transport"
"github.com/fatedier/frp/pkg/util/limit" "github.com/fatedier/frp/pkg/util/limit"
"github.com/fatedier/frp/pkg/util/xlog" "github.com/fatedier/frp/pkg/util/xlog"
"github.com/fatedier/frp/pkg/vnet"
) )
var proxyFactoryRegistry = map[reflect.Type]func(*BaseProxy, v1.ProxyConfigurer) Proxy{} var proxyFactoryRegistry = map[reflect.Type]func(*BaseProxy, v1.ProxyConfigurer) Proxy{}
@ -58,6 +59,7 @@ func NewProxy(
pxyConf v1.ProxyConfigurer, pxyConf v1.ProxyConfigurer,
clientCfg *v1.ClientCommonConfig, clientCfg *v1.ClientCommonConfig,
msgTransporter transport.MessageTransporter, msgTransporter transport.MessageTransporter,
vnetController *vnet.Controller,
) (pxy Proxy) { ) (pxy Proxy) {
var limiter *rate.Limiter var limiter *rate.Limiter
limitBytes := pxyConf.GetBaseConfig().Transport.BandwidthLimit.Bytes() limitBytes := pxyConf.GetBaseConfig().Transport.BandwidthLimit.Bytes()
@ -70,6 +72,7 @@ func NewProxy(
clientCfg: clientCfg, clientCfg: clientCfg,
limiter: limiter, limiter: limiter,
msgTransporter: msgTransporter, msgTransporter: msgTransporter,
vnetController: vnetController,
xl: xlog.FromContextSafe(ctx), xl: xlog.FromContextSafe(ctx),
ctx: ctx, ctx: ctx,
} }
@ -85,6 +88,7 @@ type BaseProxy struct {
baseCfg *v1.ProxyBaseConfig baseCfg *v1.ProxyBaseConfig
clientCfg *v1.ClientCommonConfig clientCfg *v1.ClientCommonConfig
msgTransporter transport.MessageTransporter msgTransporter transport.MessageTransporter
vnetController *vnet.Controller
limiter *rate.Limiter limiter *rate.Limiter
// proxyPlugin is used to handle connections instead of dialing to local service. // proxyPlugin is used to handle connections instead of dialing to local service.
// It's only validate for TCP protocol now. // It's only validate for TCP protocol now.
@ -98,7 +102,10 @@ type BaseProxy struct {
func (pxy *BaseProxy) Run() error { func (pxy *BaseProxy) Run() error {
if pxy.baseCfg.Plugin.Type != "" { 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.ProxyContext{
Name: pxy.baseCfg.Name,
VnetController: pxy.vnetController,
}, pxy.baseCfg.Plugin.ClientPluginOptions)
if err != nil { if err != nil {
return err 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 // 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.SrcAddr != "" && m.SrcPort != 0 {
if m.DstAddr == "" { if m.DstAddr == "" {
m.DstAddr = "127.0.0.1" m.DstAddr = "127.0.0.1"
} }
srcAddr, _ := net.ResolveTCPAddr("tcp", net.JoinHostPort(m.SrcAddr, strconv.Itoa(int(m.SrcPort)))) 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)))) dstAddr, _ := net.ResolveTCPAddr("tcp", net.JoinHostPort(m.DstAddr, strconv.Itoa(int(m.DstPort))))
extraInfo.SrcAddr = srcAddr connInfo.SrcAddr = srcAddr
extraInfo.DstAddr = dstAddr connInfo.DstAddr = dstAddr
} }
if baseCfg.Transport.ProxyProtocolVersion != "" && m.SrcAddr != "" && m.SrcPort != 0 { if baseCfg.Transport.ProxyProtocolVersion != "" && m.SrcAddr != "" && m.SrcPort != 0 {
h := &pp.Header{ h := &pp.Header{
Command: pp.PROXY, Command: pp.PROXY,
SourceAddr: extraInfo.SrcAddr, SourceAddr: connInfo.SrcAddr,
DestinationAddr: extraInfo.DstAddr, DestinationAddr: connInfo.DstAddr,
} }
if strings.Contains(m.SrcAddr, ".") { 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" { } else if baseCfg.Transport.ProxyProtocolVersion == "v2" {
h.Version = 2 h.Version = 2
} }
extraInfo.ProxyProtocolHeader = h connInfo.ProxyProtocolHeader = h
} }
connInfo.Conn = remote
connInfo.UnderlyingConn = workConn
if pxy.proxyPlugin != nil { if pxy.proxyPlugin != nil {
// if plugin is set, let plugin handle connection first // if plugin is set, let plugin handle connection first
xl.Debugf("handle by plugin: %s", pxy.proxyPlugin.Name()) 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") xl.Debugf("handle by plugin finished")
return 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(), 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()) localConn.RemoteAddr().String(), workConn.LocalAddr().String(), workConn.RemoteAddr().String())
if extraInfo.ProxyProtocolHeader != nil { if connInfo.ProxyProtocolHeader != nil {
if _, err := extraInfo.ProxyProtocolHeader.WriteTo(localConn); err != nil { if _, err := connInfo.ProxyProtocolHeader.WriteTo(localConn); err != nil {
workConn.Close() workConn.Close()
xl.Errorf("write proxy protocol header to local conn error: %v", err) xl.Errorf("write proxy protocol header to local conn error: %v", err)
return return

View File

@ -28,12 +28,14 @@ import (
"github.com/fatedier/frp/pkg/msg" "github.com/fatedier/frp/pkg/msg"
"github.com/fatedier/frp/pkg/transport" "github.com/fatedier/frp/pkg/transport"
"github.com/fatedier/frp/pkg/util/xlog" "github.com/fatedier/frp/pkg/util/xlog"
"github.com/fatedier/frp/pkg/vnet"
) )
type Manager struct { type Manager struct {
proxies map[string]*Wrapper proxies map[string]*Wrapper
msgTransporter transport.MessageTransporter msgTransporter transport.MessageTransporter
inWorkConnCallback func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) bool inWorkConnCallback func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) bool
vnetController *vnet.Controller
closed bool closed bool
mu sync.RWMutex mu sync.RWMutex
@ -47,10 +49,12 @@ func NewManager(
ctx context.Context, ctx context.Context,
clientCfg *v1.ClientCommonConfig, clientCfg *v1.ClientCommonConfig,
msgTransporter transport.MessageTransporter, msgTransporter transport.MessageTransporter,
vnetController *vnet.Controller,
) *Manager { ) *Manager {
return &Manager{ return &Manager{
proxies: make(map[string]*Wrapper), proxies: make(map[string]*Wrapper),
msgTransporter: msgTransporter, msgTransporter: msgTransporter,
vnetController: vnetController,
closed: false, closed: false,
clientCfg: clientCfg, clientCfg: clientCfg,
ctx: ctx, ctx: ctx,
@ -159,7 +163,7 @@ func (pm *Manager) UpdateAll(proxyCfgs []v1.ProxyConfigurer) {
for _, cfg := range proxyCfgs { for _, cfg := range proxyCfgs {
name := cfg.GetBaseConfig().Name name := cfg.GetBaseConfig().Name
if _, ok := pm.proxies[name]; !ok { 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 { if pm.inWorkConnCallback != nil {
pxy.SetInWorkConnCallback(pm.inWorkConnCallback) pxy.SetInWorkConnCallback(pm.inWorkConnCallback)
} }

View File

@ -31,6 +31,7 @@ import (
"github.com/fatedier/frp/pkg/msg" "github.com/fatedier/frp/pkg/msg"
"github.com/fatedier/frp/pkg/transport" "github.com/fatedier/frp/pkg/transport"
"github.com/fatedier/frp/pkg/util/xlog" "github.com/fatedier/frp/pkg/util/xlog"
"github.com/fatedier/frp/pkg/vnet"
) )
const ( const (
@ -73,6 +74,8 @@ type Wrapper struct {
handler event.Handler handler event.Handler
msgTransporter transport.MessageTransporter msgTransporter transport.MessageTransporter
// vnet controller
vnetController *vnet.Controller
health uint32 health uint32
lastSendStartMsg time.Time lastSendStartMsg time.Time
@ -91,6 +94,7 @@ func NewWrapper(
clientCfg *v1.ClientCommonConfig, clientCfg *v1.ClientCommonConfig,
eventHandler event.Handler, eventHandler event.Handler,
msgTransporter transport.MessageTransporter, msgTransporter transport.MessageTransporter,
vnetController *vnet.Controller,
) *Wrapper { ) *Wrapper {
baseInfo := cfg.GetBaseConfig() baseInfo := cfg.GetBaseConfig()
xl := xlog.FromContextSafe(ctx).Spawn().AppendPrefix(baseInfo.Name) xl := xlog.FromContextSafe(ctx).Spawn().AppendPrefix(baseInfo.Name)
@ -105,6 +109,7 @@ func NewWrapper(
healthNotifyCh: make(chan struct{}), healthNotifyCh: make(chan struct{}),
handler: eventHandler, handler: eventHandler,
msgTransporter: msgTransporter, msgTransporter: msgTransporter,
vnetController: vnetController,
xl: xl, xl: xl,
ctx: xlog.NewContext(ctx, xl), ctx: xlog.NewContext(ctx, xl),
} }
@ -117,7 +122,7 @@ func NewWrapper(
xl.Tracef("enable health check monitor") 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 return pw
} }

View File

@ -37,6 +37,7 @@ import (
"github.com/fatedier/frp/pkg/util/version" "github.com/fatedier/frp/pkg/util/version"
"github.com/fatedier/frp/pkg/util/wait" "github.com/fatedier/frp/pkg/util/wait"
"github.com/fatedier/frp/pkg/util/xlog" "github.com/fatedier/frp/pkg/util/xlog"
"github.com/fatedier/frp/pkg/vnet"
) )
func init() { func init() {
@ -110,6 +111,8 @@ type Service struct {
// web server for admin UI and apis // web server for admin UI and apis
webServer *httppkg.Server webServer *httppkg.Server
vnetController *vnet.Controller
cfgMu sync.RWMutex cfgMu sync.RWMutex
common *v1.ClientCommonConfig common *v1.ClientCommonConfig
proxyCfgs []v1.ProxyConfigurer proxyCfgs []v1.ProxyConfigurer
@ -156,6 +159,9 @@ func NewService(options ServiceOptions) (*Service, error) {
if webServer != nil { if webServer != nil {
webServer.RouteRegister(s.registerRouteHandlers) webServer.RouteRegister(s.registerRouteHandlers)
} }
if options.Common.VirtualNet.Address != "" {
s.vnetController = vnet.NewController(options.Common.VirtualNet)
}
return s, nil return s, nil
} }
@ -169,6 +175,19 @@ func (svr *Service) Run(ctx context.Context) error {
netpkg.SetDefaultDNSAddress(svr.common.DNSServer) 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 { if svr.webServer != nil {
go func() { go func() {
log.Infof("admin server listen on %s", svr.webServer.Address()) log.Infof("admin server listen on %s", svr.webServer.Address())
@ -317,6 +336,7 @@ func (svr *Service) loopLoginUntilSuccess(maxInterval time.Duration, firstLoginE
ConnEncrypted: connEncrypted, ConnEncrypted: connEncrypted,
AuthSetter: svr.authSetter, AuthSetter: svr.authSetter,
Connector: connector, Connector: connector,
VnetController: svr.vnetController,
} }
ctl, err := NewControl(svr.ctx, sessionCtx) ctl, err := NewControl(svr.ctx, sessionCtx)
if err != nil { if err != nil {

View File

@ -44,6 +44,8 @@ func (sv *STCPVisitor) Run() (err error) {
} }
go sv.internalConnWorker() go sv.internalConnWorker()
sv.plugin.Start()
return return
} }

View File

@ -20,9 +20,11 @@ import (
"sync" "sync"
v1 "github.com/fatedier/frp/pkg/config/v1" v1 "github.com/fatedier/frp/pkg/config/v1"
plugin "github.com/fatedier/frp/pkg/plugin/visitor"
"github.com/fatedier/frp/pkg/transport" "github.com/fatedier/frp/pkg/transport"
netpkg "github.com/fatedier/frp/pkg/util/net" netpkg "github.com/fatedier/frp/pkg/util/net"
"github.com/fatedier/frp/pkg/util/xlog" "github.com/fatedier/frp/pkg/util/xlog"
"github.com/fatedier/frp/pkg/vnet"
) )
// Helper wraps some functions for visitor to use. // 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 // MsgTransporter returns the message transporter that is used to send and receive messages
// to the frp server through the controller. // to the frp server through the controller.
MsgTransporter() transport.MessageTransporter 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 returns the run id of current controller.
RunID() string RunID() string
} }
@ -50,14 +54,34 @@ func NewVisitor(
cfg v1.VisitorConfigurer, cfg v1.VisitorConfigurer,
clientCfg *v1.ClientCommonConfig, clientCfg *v1.ClientCommonConfig,
helper Helper, helper Helper,
) (visitor Visitor) { ) (Visitor, error) {
xl := xlog.FromContextSafe(ctx).Spawn().AppendPrefix(cfg.GetBaseConfig().Name) xl := xlog.FromContextSafe(ctx).Spawn().AppendPrefix(cfg.GetBaseConfig().Name)
ctx = xlog.NewContext(ctx, xl)
var visitor Visitor
baseVisitor := BaseVisitor{ baseVisitor := BaseVisitor{
clientCfg: clientCfg, clientCfg: clientCfg,
helper: helper, helper: helper,
ctx: xlog.NewContext(ctx, xl), ctx: ctx,
internalLn: netpkg.NewInternalListener(), internalLn: netpkg.NewInternalListener(),
} }
if cfg.GetBaseConfig().Plugin.Type != "" {
p, err := plugin.Create(
cfg.GetBaseConfig().Plugin.Type,
plugin.VisitorContext{
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) { switch cfg := cfg.(type) {
case *v1.STCPVisitorConfig: case *v1.STCPVisitorConfig:
visitor = &STCPVisitor{ visitor = &STCPVisitor{
@ -77,7 +101,7 @@ func NewVisitor(
checkCloseCh: make(chan struct{}), checkCloseCh: make(chan struct{}),
} }
} }
return return visitor, nil
} }
type BaseVisitor struct { type BaseVisitor struct {
@ -85,6 +109,7 @@ type BaseVisitor struct {
helper Helper helper Helper
l net.Listener l net.Listener
internalLn *netpkg.InternalListener internalLn *netpkg.InternalListener
plugin plugin.Plugin
mu sync.RWMutex mu sync.RWMutex
ctx context.Context ctx context.Context
@ -101,4 +126,7 @@ func (v *BaseVisitor) Close() {
if v.internalLn != nil { if v.internalLn != nil {
v.internalLn.Close() v.internalLn.Close()
} }
if v.plugin != nil {
v.plugin.Close()
}
} }

View File

@ -27,6 +27,7 @@ import (
v1 "github.com/fatedier/frp/pkg/config/v1" v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/transport" "github.com/fatedier/frp/pkg/transport"
"github.com/fatedier/frp/pkg/util/xlog" "github.com/fatedier/frp/pkg/util/xlog"
"github.com/fatedier/frp/pkg/vnet"
) )
type Manager struct { type Manager struct {
@ -50,6 +51,7 @@ func NewManager(
clientCfg *v1.ClientCommonConfig, clientCfg *v1.ClientCommonConfig,
connectServer func() (net.Conn, error), connectServer func() (net.Conn, error),
msgTransporter transport.MessageTransporter, msgTransporter transport.MessageTransporter,
vnetController *vnet.Controller,
) *Manager { ) *Manager {
m := &Manager{ m := &Manager{
clientCfg: clientCfg, clientCfg: clientCfg,
@ -62,6 +64,7 @@ func NewManager(
m.helper = &visitorHelperImpl{ m.helper = &visitorHelperImpl{
connectServerFn: connectServer, connectServerFn: connectServer,
msgTransporter: msgTransporter, msgTransporter: msgTransporter,
vnetController: vnetController,
transferConnFn: m.TransferConn, transferConnFn: m.TransferConn,
runID: runID, runID: runID,
} }
@ -112,7 +115,11 @@ func (vm *Manager) Close() {
func (vm *Manager) startVisitor(cfg v1.VisitorConfigurer) (err error) { func (vm *Manager) startVisitor(cfg v1.VisitorConfigurer) (err error) {
xl := xlog.FromContextSafe(vm.ctx) xl := xlog.FromContextSafe(vm.ctx)
name := cfg.GetBaseConfig().Name 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() err = visitor.Run()
if err != nil { if err != nil {
xl.Warnf("start error: %v", err) xl.Warnf("start error: %v", err)
@ -187,6 +194,7 @@ func (vm *Manager) TransferConn(name string, conn net.Conn) error {
type visitorHelperImpl struct { type visitorHelperImpl struct {
connectServerFn func() (net.Conn, error) connectServerFn func() (net.Conn, error)
msgTransporter transport.MessageTransporter msgTransporter transport.MessageTransporter
vnetController *vnet.Controller
transferConnFn func(name string, conn net.Conn) error transferConnFn func(name string, conn net.Conn) error
runID string runID string
} }
@ -203,6 +211,10 @@ func (v *visitorHelperImpl) MsgTransporter() transport.MessageTransporter {
return v.msgTransporter return v.msgTransporter
} }
func (v *visitorHelperImpl) VNetController() *vnet.Controller {
return v.vnetController
}
func (v *visitorHelperImpl) RunID() string { func (v *visitorHelperImpl) RunID() string {
return v.runID return v.runID
} }

View File

@ -73,6 +73,8 @@ func (sv *XTCPVisitor) Run() (err error) {
sv.retryLimiter = rate.NewLimiter(rate.Every(time.Hour/time.Duration(sv.cfg.MaxRetriesAnHour)), sv.cfg.MaxRetriesAnHour) sv.retryLimiter = rate.NewLimiter(rate.Every(time.Hour/time.Duration(sv.cfg.MaxRetriesAnHour)), sv.cfg.MaxRetriesAnHour)
go sv.keepTunnelOpenWorker() go sv.keepTunnelOpenWorker()
} }
sv.plugin.Start()
return return
} }
@ -157,9 +159,9 @@ func (sv *XTCPVisitor) keepTunnelOpenWorker() {
func (sv *XTCPVisitor) handleConn(userConn net.Conn) { func (sv *XTCPVisitor) handleConn(userConn net.Conn) {
xl := xlog.FromContextSafe(sv.ctx) xl := xlog.FromContextSafe(sv.ctx)
isConnTrasfered := false isConnTransfered := false
defer func() { defer func() {
if !isConnTrasfered { if !isConnTransfered {
userConn.Close() userConn.Close()
} }
}() }()
@ -187,7 +189,7 @@ func (sv *XTCPVisitor) handleConn(userConn net.Conn) {
xl.Errorf("transfer connection to visitor %s error: %v", sv.cfg.FallbackTo, err) xl.Errorf("transfer connection to visitor %s error: %v", sv.cfg.FallbackTo, err)
return return
} }
isConnTrasfered = true isConnTransfered = true
return return
} }

5
go.mod
View File

@ -19,16 +19,19 @@ require (
github.com/quic-go/quic-go v0.48.2 github.com/quic-go/quic-go v0.48.2
github.com/rodaine/table v1.2.0 github.com/rodaine/table v1.2.0
github.com/samber/lo v1.47.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/cobra v1.8.0
github.com/spf13/pflag v1.0.5 github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.9.0 github.com/stretchr/testify v1.9.0
github.com/tidwall/gjson v1.17.1 github.com/tidwall/gjson v1.17.1
github.com/vishvananda/netlink v1.3.0
github.com/xtaci/kcp-go/v5 v5.6.13 github.com/xtaci/kcp-go/v5 v5.6.13
golang.org/x/crypto v0.30.0 golang.org/x/crypto v0.30.0
golang.org/x/net v0.32.0 golang.org/x/net v0.32.0
golang.org/x/oauth2 v0.16.0 golang.org/x/oauth2 v0.16.0
golang.org/x/sync v0.10.0 golang.org/x/sync v0.10.0
golang.org/x/time v0.5.0 golang.org/x/time v0.5.0
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
gopkg.in/ini.v1 v1.67.0 gopkg.in/ini.v1 v1.67.0
k8s.io/apimachinery v0.28.8 k8s.io/apimachinery v0.28.8
k8s.io/client-go v0.28.8 k8s.io/client-go v0.28.8
@ -64,12 +67,14 @@ require (
github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect github.com/tidwall/pretty v1.2.0 // indirect
github.com/tjfoc/gmsm v1.4.1 // 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 go.uber.org/mock v0.5.0 // indirect
golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d // indirect golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d // indirect
golang.org/x/mod v0.22.0 // indirect golang.org/x/mod v0.22.0 // indirect
golang.org/x/sys v0.28.0 // indirect golang.org/x/sys v0.28.0 // indirect
golang.org/x/text v0.21.0 // indirect golang.org/x/text v0.21.0 // indirect
golang.org/x/tools v0.28.0 // indirect golang.org/x/tools v0.28.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
google.golang.org/appengine v1.6.8 // indirect google.golang.org/appengine v1.6.8 // indirect
google.golang.org/protobuf v1.34.1 // indirect google.golang.org/protobuf v1.34.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect

16
go.sum
View File

@ -48,6 +48,8 @@ github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 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 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 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.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.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.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
@ -117,6 +119,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/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 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc=
github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU= 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 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
@ -143,6 +147,10 @@ github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= 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 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE= 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 h1:FEjtz9+D4p8t2x4WjciGt/jsIuhlWjjgPCCWjrVR4Hk=
github.com/xtaci/kcp-go/v5 v5.6.13/go.mod h1:75S1AKYYzNUSXIv30h+jPKJYZUwqpfvLshu63nCNSOM= 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= github.com/xtaci/lossyconn v0.0.0-20200209145036-adba10fffc37 h1:EWU6Pktpas0n8lLQwDsRyZfmkPeRbdgPtW609es+/9E=
@ -201,9 +209,11 @@ 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-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-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.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.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.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.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.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= 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.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
@ -238,6 +248,10 @@ golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8=
golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw= 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-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/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.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.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 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
@ -268,6 +282,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.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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/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= k8s.io/apimachinery v0.28.8 h1:hi/nrxHwk4QLV+W/SHve1bypTE59HCDorLY1stBIxKQ=

View File

@ -61,6 +61,7 @@ type ClientCommonConfig struct {
Log LogConfig `json:"log,omitempty"` Log LogConfig `json:"log,omitempty"`
WebServer WebServerConfig `json:"webServer,omitempty"` WebServer WebServerConfig `json:"webServer,omitempty"`
Transport ClientTransportConfig `json:"transport,omitempty"` Transport ClientTransportConfig `json:"transport,omitempty"`
VirtualNet VirtualNetConfig `json:"virtualNet,omitempty"`
// UDPPacketSize specifies the udp packet size // UDPPacketSize specifies the udp packet size
// By default, this value is 1500 // By default, this value is 1500
@ -204,3 +205,8 @@ type AuthOIDCClientConfig struct {
// this field will be transfer to map[string][]string in OIDC token generator. // this field will be transfer to map[string][]string in OIDC token generator.
AdditionalEndpointParams map[string]string `json:"additionalEndpointParams,omitempty"` AdditionalEndpointParams map[string]string `json:"additionalEndpointParams,omitempty"`
} }
type VirtualNetConfig struct {
Address string `json:"address,omitempty"`
Routes []string `json:"routes,omitempty"`
}

View File

@ -26,6 +26,32 @@ import (
"github.com/fatedier/frp/pkg/util/util" "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 { type ClientPluginOptions interface {
Complete() Complete()
} }
@ -74,30 +100,6 @@ func (c *TypedClientPluginOptions) MarshalJSON() ([]byte, error) {
return json.Marshal(c.ClientPluginOptions) 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 HTTP2HTTPSPluginOptions struct {
Type string `json:"type,omitempty"` Type string `json:"type,omitempty"`
LocalAddr string `json:"localAddr,omitempty"` LocalAddr string `json:"localAddr,omitempty"`
@ -185,3 +187,10 @@ type TLS2RawPluginOptions struct {
} }
func (o *TLS2RawPluginOptions) Complete() {} func (o *TLS2RawPluginOptions) Complete() {}
type VirtualNetPluginOptions struct {
Type string `json:"type,omitempty"`
AllowedIPs []string `json:"allowedIPs,omitempty"`
}
func (o *VirtualNetPluginOptions) Complete() {}

View File

@ -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 // 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) // other visitors. (This is not supported for SUDP now)
BindPort int `json:"bindPort,omitempty"` BindPort int `json:"bindPort,omitempty"`
// Plugin specifies what plugin should be used.
Plugin TypedVisitorPluginOptions `json:"plugin,omitempty"`
} }
func (c *VisitorBaseConfig) GetBaseConfig() *VisitorBaseConfig { func (c *VisitorBaseConfig) GetBaseConfig() *VisitorBaseConfig {

View File

@ -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() {}

View File

@ -18,9 +18,7 @@ package plugin
import ( import (
"context" "context"
"io"
stdlog "log" stdlog "log"
"net"
"net/http" "net/http"
"net/http/httputil" "net/http/httputil"
@ -42,7 +40,7 @@ type HTTP2HTTPPlugin struct {
s *http.Server s *http.Server
} }
func NewHTTP2HTTPPlugin(options v1.ClientPluginOptions) (Plugin, error) { func NewHTTP2HTTPPlugin(_ ProxyContext, options v1.ClientPluginOptions) (Plugin, error) {
opts := options.(*v1.HTTP2HTTPPluginOptions) opts := options.(*v1.HTTP2HTTPPluginOptions)
listener := NewProxyListener() listener := NewProxyListener()
@ -80,8 +78,8 @@ func NewHTTP2HTTPPlugin(options v1.ClientPluginOptions) (Plugin, error) {
return p, nil return p, nil
} }
func (p *HTTP2HTTPPlugin) Handle(_ context.Context, conn io.ReadWriteCloser, realConn net.Conn, _ *ExtraInfo) { func (p *HTTP2HTTPPlugin) Handle(_ context.Context, connInfo *ConnectionInfo) {
wrapConn := netpkg.WrapReadWriteCloserToConn(conn, realConn) wrapConn := netpkg.WrapReadWriteCloserToConn(connInfo.Conn, connInfo.UnderlyingConn)
_ = p.l.PutConn(wrapConn) _ = p.l.PutConn(wrapConn)
} }

View File

@ -19,9 +19,7 @@ package plugin
import ( import (
"context" "context"
"crypto/tls" "crypto/tls"
"io"
stdlog "log" stdlog "log"
"net"
"net/http" "net/http"
"net/http/httputil" "net/http/httputil"
@ -43,7 +41,7 @@ type HTTP2HTTPSPlugin struct {
s *http.Server s *http.Server
} }
func NewHTTP2HTTPSPlugin(options v1.ClientPluginOptions) (Plugin, error) { func NewHTTP2HTTPSPlugin(_ ProxyContext, options v1.ClientPluginOptions) (Plugin, error) {
opts := options.(*v1.HTTP2HTTPSPluginOptions) opts := options.(*v1.HTTP2HTTPSPluginOptions)
listener := NewProxyListener() listener := NewProxyListener()
@ -89,8 +87,8 @@ func NewHTTP2HTTPSPlugin(options v1.ClientPluginOptions) (Plugin, error) {
return p, nil return p, nil
} }
func (p *HTTP2HTTPSPlugin) Handle(_ context.Context, conn io.ReadWriteCloser, realConn net.Conn, _ *ExtraInfo) { func (p *HTTP2HTTPSPlugin) Handle(_ context.Context, connInfo *ConnectionInfo) {
wrapConn := netpkg.WrapReadWriteCloserToConn(conn, realConn) wrapConn := netpkg.WrapReadWriteCloserToConn(connInfo.Conn, connInfo.UnderlyingConn)
_ = p.l.PutConn(wrapConn) _ = p.l.PutConn(wrapConn)
} }

View File

@ -45,7 +45,7 @@ type HTTPProxy struct {
s *http.Server s *http.Server
} }
func NewHTTPProxyPlugin(options v1.ClientPluginOptions) (Plugin, error) { func NewHTTPProxyPlugin(_ ProxyContext, options v1.ClientPluginOptions) (Plugin, error) {
opts := options.(*v1.HTTPProxyPluginOptions) opts := options.(*v1.HTTPProxyPluginOptions)
listener := NewProxyListener() listener := NewProxyListener()
@ -69,8 +69,8 @@ func (hp *HTTPProxy) Name() string {
return v1.PluginHTTPProxy return v1.PluginHTTPProxy
} }
func (hp *HTTPProxy) Handle(_ context.Context, conn io.ReadWriteCloser, realConn net.Conn, _ *ExtraInfo) { func (hp *HTTPProxy) Handle(_ context.Context, connInfo *ConnectionInfo) {
wrapConn := netpkg.WrapReadWriteCloserToConn(conn, realConn) wrapConn := netpkg.WrapReadWriteCloserToConn(connInfo.Conn, connInfo.UnderlyingConn)
sc, rd := libnet.NewSharedConn(wrapConn) sc, rd := libnet.NewSharedConn(wrapConn)
firstBytes := make([]byte, 7) firstBytes := make([]byte, 7)

View File

@ -20,9 +20,7 @@ import (
"context" "context"
"crypto/tls" "crypto/tls"
"fmt" "fmt"
"io"
stdlog "log" stdlog "log"
"net"
"net/http" "net/http"
"net/http/httputil" "net/http/httputil"
"time" "time"
@ -48,7 +46,7 @@ type HTTPS2HTTPPlugin struct {
s *http.Server s *http.Server
} }
func NewHTTPS2HTTPPlugin(options v1.ClientPluginOptions) (Plugin, error) { func NewHTTPS2HTTPPlugin(_ ProxyContext, options v1.ClientPluginOptions) (Plugin, error) {
opts := options.(*v1.HTTPS2HTTPPluginOptions) opts := options.(*v1.HTTPS2HTTPPluginOptions)
listener := NewProxyListener() listener := NewProxyListener()
@ -106,10 +104,10 @@ func NewHTTPS2HTTPPlugin(options v1.ClientPluginOptions) (Plugin, error) {
return p, nil return p, nil
} }
func (p *HTTPS2HTTPPlugin) Handle(_ context.Context, conn io.ReadWriteCloser, realConn net.Conn, extra *ExtraInfo) { func (p *HTTPS2HTTPPlugin) Handle(_ context.Context, connInfo *ConnectionInfo) {
wrapConn := netpkg.WrapReadWriteCloserToConn(conn, realConn) wrapConn := netpkg.WrapReadWriteCloserToConn(connInfo.Conn, connInfo.UnderlyingConn)
if extra.SrcAddr != nil { if connInfo.SrcAddr != nil {
wrapConn.SetRemoteAddr(extra.SrcAddr) wrapConn.SetRemoteAddr(connInfo.SrcAddr)
} }
_ = p.l.PutConn(wrapConn) _ = p.l.PutConn(wrapConn)
} }

View File

@ -20,9 +20,7 @@ import (
"context" "context"
"crypto/tls" "crypto/tls"
"fmt" "fmt"
"io"
stdlog "log" stdlog "log"
"net"
"net/http" "net/http"
"net/http/httputil" "net/http/httputil"
"time" "time"
@ -48,7 +46,7 @@ type HTTPS2HTTPSPlugin struct {
s *http.Server s *http.Server
} }
func NewHTTPS2HTTPSPlugin(options v1.ClientPluginOptions) (Plugin, error) { func NewHTTPS2HTTPSPlugin(_ ProxyContext, options v1.ClientPluginOptions) (Plugin, error) {
opts := options.(*v1.HTTPS2HTTPSPluginOptions) opts := options.(*v1.HTTPS2HTTPSPluginOptions)
listener := NewProxyListener() listener := NewProxyListener()
@ -112,10 +110,10 @@ func NewHTTPS2HTTPSPlugin(options v1.ClientPluginOptions) (Plugin, error) {
return p, nil return p, nil
} }
func (p *HTTPS2HTTPSPlugin) Handle(_ context.Context, conn io.ReadWriteCloser, realConn net.Conn, extra *ExtraInfo) { func (p *HTTPS2HTTPSPlugin) Handle(_ context.Context, connInfo *ConnectionInfo) {
wrapConn := netpkg.WrapReadWriteCloserToConn(conn, realConn) wrapConn := netpkg.WrapReadWriteCloserToConn(connInfo.Conn, connInfo.UnderlyingConn)
if extra.SrcAddr != nil { if connInfo.SrcAddr != nil {
wrapConn.SetRemoteAddr(extra.SrcAddr) wrapConn.SetRemoteAddr(connInfo.SrcAddr)
} }
_ = p.l.PutConn(wrapConn) _ = p.l.PutConn(wrapConn)
} }

View File

@ -25,13 +25,18 @@ import (
pp "github.com/pires/go-proxyproto" pp "github.com/pires/go-proxyproto"
v1 "github.com/fatedier/frp/pkg/config/v1" v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/vnet"
) )
type ProxyContext struct {
Name string
VnetController *vnet.Controller
}
// Creators is used for create plugins to handle connections. // Creators is used for create plugins to handle connections.
var creators = make(map[string]CreatorFn) var creators = make(map[string]CreatorFn)
// params has prefix "plugin_" type CreatorFn func(pxyCtx ProxyContext, options v1.ClientPluginOptions) (Plugin, error)
type CreatorFn func(options v1.ClientPluginOptions) (Plugin, error)
func Register(name string, fn CreatorFn) { func Register(name string, fn CreatorFn) {
if _, exist := creators[name]; exist { if _, exist := creators[name]; exist {
@ -40,16 +45,19 @@ func Register(name string, fn CreatorFn) {
creators[name] = fn creators[name] = fn
} }
func Create(name string, options v1.ClientPluginOptions) (p Plugin, err error) { func Create(pluginName string, pxyCtx ProxyContext, options v1.ClientPluginOptions) (p Plugin, err error) {
if fn, ok := creators[name]; ok { if fn, ok := creators[pluginName]; ok {
p, err = fn(options) p, err = fn(pxyCtx, options)
} else { } else {
err = fmt.Errorf("plugin [%s] is not registered", name) err = fmt.Errorf("plugin [%s] is not registered", pluginName)
} }
return return
} }
type ExtraInfo struct { type ConnectionInfo struct {
Conn io.ReadWriteCloser
UnderlyingConn net.Conn
ProxyProtocolHeader *pp.Header ProxyProtocolHeader *pp.Header
SrcAddr net.Addr SrcAddr net.Addr
DstAddr net.Addr DstAddr net.Addr
@ -58,7 +66,7 @@ type ExtraInfo struct {
type Plugin interface { type Plugin interface {
Name() string Name() string
Handle(ctx context.Context, conn io.ReadWriteCloser, realConn net.Conn, extra *ExtraInfo) Handle(ctx context.Context, connInfo *ConnectionInfo)
Close() error Close() error
} }

View File

@ -20,7 +20,6 @@ import (
"context" "context"
"io" "io"
"log" "log"
"net"
gosocks5 "github.com/armon/go-socks5" gosocks5 "github.com/armon/go-socks5"
@ -36,7 +35,7 @@ type Socks5Plugin struct {
Server *gosocks5.Server Server *gosocks5.Server
} }
func NewSocks5Plugin(options v1.ClientPluginOptions) (p Plugin, err error) { func NewSocks5Plugin(_ ProxyContext, options v1.ClientPluginOptions) (p Plugin, err error) {
opts := options.(*v1.Socks5PluginOptions) opts := options.(*v1.Socks5PluginOptions)
cfg := &gosocks5.Config{ cfg := &gosocks5.Config{
@ -51,9 +50,9 @@ func NewSocks5Plugin(options v1.ClientPluginOptions) (p Plugin, err error) {
return return
} }
func (sp *Socks5Plugin) Handle(_ context.Context, conn io.ReadWriteCloser, realConn net.Conn, _ *ExtraInfo) { func (sp *Socks5Plugin) Handle(_ context.Context, connInfo *ConnectionInfo) {
defer conn.Close() defer connInfo.Conn.Close()
wrapConn := netpkg.WrapReadWriteCloserToConn(conn, realConn) wrapConn := netpkg.WrapReadWriteCloserToConn(connInfo.Conn, connInfo.UnderlyingConn)
_ = sp.Server.ServeConn(wrapConn) _ = sp.Server.ServeConn(wrapConn)
} }

View File

@ -18,8 +18,6 @@ package plugin
import ( import (
"context" "context"
"io"
"net"
"net/http" "net/http"
"time" "time"
@ -40,7 +38,7 @@ type StaticFilePlugin struct {
s *http.Server s *http.Server
} }
func NewStaticFilePlugin(options v1.ClientPluginOptions) (Plugin, error) { func NewStaticFilePlugin(_ ProxyContext, options v1.ClientPluginOptions) (Plugin, error) {
opts := options.(*v1.StaticFilePluginOptions) opts := options.(*v1.StaticFilePluginOptions)
listener := NewProxyListener() listener := NewProxyListener()
@ -70,8 +68,8 @@ func NewStaticFilePlugin(options v1.ClientPluginOptions) (Plugin, error) {
return sp, nil return sp, nil
} }
func (sp *StaticFilePlugin) Handle(_ context.Context, conn io.ReadWriteCloser, realConn net.Conn, _ *ExtraInfo) { func (sp *StaticFilePlugin) Handle(_ context.Context, connInfo *ConnectionInfo) {
wrapConn := netpkg.WrapReadWriteCloserToConn(conn, realConn) wrapConn := netpkg.WrapReadWriteCloserToConn(connInfo.Conn, connInfo.UnderlyingConn)
_ = sp.l.PutConn(wrapConn) _ = sp.l.PutConn(wrapConn)
} }

View File

@ -19,7 +19,6 @@ package plugin
import ( import (
"context" "context"
"crypto/tls" "crypto/tls"
"io"
"net" "net"
libio "github.com/fatedier/golib/io" libio "github.com/fatedier/golib/io"
@ -40,7 +39,7 @@ type TLS2RawPlugin struct {
tlsConfig *tls.Config tlsConfig *tls.Config
} }
func NewTLS2RawPlugin(options v1.ClientPluginOptions) (Plugin, error) { func NewTLS2RawPlugin(_ ProxyContext, options v1.ClientPluginOptions) (Plugin, error) {
opts := options.(*v1.TLS2RawPluginOptions) opts := options.(*v1.TLS2RawPluginOptions)
p := &TLS2RawPlugin{ p := &TLS2RawPlugin{
@ -55,10 +54,10 @@ func NewTLS2RawPlugin(options v1.ClientPluginOptions) (Plugin, error) {
return p, nil 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) xl := xlog.FromContextSafe(ctx)
wrapConn := netpkg.WrapReadWriteCloserToConn(conn, realConn) wrapConn := netpkg.WrapReadWriteCloserToConn(connInfo.Conn, connInfo.UnderlyingConn)
tlsConn := tls.Server(wrapConn, p.tlsConfig) tlsConn := tls.Server(wrapConn, p.tlsConfig)
if err := tlsConn.Handshake(); err != nil { if err := tlsConn.Handshake(); err != nil {

View File

@ -18,7 +18,6 @@ package plugin
import ( import (
"context" "context"
"io"
"net" "net"
libio "github.com/fatedier/golib/io" libio "github.com/fatedier/golib/io"
@ -35,7 +34,7 @@ type UnixDomainSocketPlugin struct {
UnixAddr *net.UnixAddr UnixAddr *net.UnixAddr
} }
func NewUnixDomainSocketPlugin(options v1.ClientPluginOptions) (p Plugin, err error) { func NewUnixDomainSocketPlugin(_ ProxyContext, options v1.ClientPluginOptions) (p Plugin, err error) {
opts := options.(*v1.UnixDomainSocketPluginOptions) opts := options.(*v1.UnixDomainSocketPluginOptions)
unixAddr, errRet := net.ResolveUnixAddr("unix", opts.UnixPath) unixAddr, errRet := net.ResolveUnixAddr("unix", opts.UnixPath)
@ -50,20 +49,20 @@ func NewUnixDomainSocketPlugin(options v1.ClientPluginOptions) (p Plugin, err er
return 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) xl := xlog.FromContextSafe(ctx)
localConn, err := net.DialUnix("unix", nil, uds.UnixAddr) localConn, err := net.DialUnix("unix", nil, uds.UnixAddr)
if err != nil { if err != nil {
xl.Warnf("dial to uds %s error: %v", uds.UnixAddr, err) xl.Warnf("dial to uds %s error: %v", uds.UnixAddr, err)
return return
} }
if extra.ProxyProtocolHeader != nil { if connInfo.ProxyProtocolHeader != nil {
if _, err := extra.ProxyProtocolHeader.WriteTo(localConn); err != nil { if _, err := connInfo.ProxyProtocolHeader.WriteTo(localConn); err != nil {
return return
} }
} }
libio.Join(localConn, conn) libio.Join(localConn, connInfo.Conn)
} }
func (uds *UnixDomainSocketPlugin) Name() string { func (uds *UnixDomainSocketPlugin) Name() string {

View File

@ -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 plugin
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 {
pxyCtx ProxyContext
opts *v1.VirtualNetPluginOptions
}
func NewVirtualNetPlugin(pxyCtx ProxyContext, options v1.ClientPluginOptions) (Plugin, error) {
opts := options.(*v1.VirtualNetPluginOptions)
p := &VirtualNetPlugin{
pxyCtx: pxyCtx,
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.pxyCtx.VnetController == nil {
return
}
// Register the connection with the controller
routeName := p.pxyCtx.Name
err := p.pxyCtx.VnetController.RegisterServerConn(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.pxyCtx.VnetController != nil {
p.pxyCtx.VnetController.UnregisterServerConn(p.pxyCtx.Name)
}
return nil
}

View File

@ -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 VisitorContext 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(visitorCtx VisitorContext, 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, visitorCtx VisitorContext, options v1.VisitorPluginOptions) (p Plugin, err error) {
if fn, ok := creators[pluginName]; ok {
p, err = fn(visitorCtx, options)
} else {
err = fmt.Errorf("plugin [%s] is not registered", pluginName)
}
return
}
type Plugin interface {
Name() string
Start()
Close() error
}

View File

@ -0,0 +1,234 @@
// 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 {
visitorCtx VisitorContext
routes []net.IPNet
mu sync.Mutex
controllerConn net.Conn
closeSignal chan struct{}
pluginCtx context.Context
pluginCancel context.CancelFunc
}
func NewVirtualNetPlugin(visitorCtx VisitorContext, options v1.VisitorPluginOptions) (Plugin, error) {
opts := options.(*v1.VirtualNetVisitorPluginOptions)
p := &VirtualNetPlugin{
visitorCtx: visitorCtx,
routes: make([]net.IPNet, 0),
}
p.pluginCtx, p.pluginCancel = context.WithCancel(visitorCtx.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.visitorCtx.Ctx)
if p.visitorCtx.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.visitorCtx.Name, routeStr)
go p.run()
}
func (p *VirtualNetPlugin) run() {
xl := xlog.FromContextSafe(p.pluginCtx)
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.pluginCtx.Done():
xl.Infof("VirtualNetPlugin run loop for visitor [%s] stopping (context cancelled before pipe creation).", p.visitorCtx.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.visitorCtx.Name)
err := p.visitorCtx.VnetController.RegisterClientRoute(p.visitorCtx.Name, p.routes, controllerConn)
if err != nil {
xl.Errorf("Failed to register client route for visitor [%s]: %v. Retrying after %v", p.visitorCtx.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.pluginCtx.Done():
xl.Infof("VirtualNetPlugin registration retry wait interrupted for visitor [%s]", p.visitorCtx.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.visitorCtx.Name)
// Pass the CloseNotifyConn to HandleConn.
// HandleConn is responsible for calling Close() on pluginNotifyConn.
p.visitorCtx.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.pluginCtx.Done():
xl.Infof("VirtualNetPlugin run loop stopping for visitor [%s] (context cancelled while waiting).", p.visitorCtx.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.visitorCtx.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.visitorCtx.Name)
select {
case <-time.After(reconnectDelay):
// Delay completed, loop will continue.
case <-p.pluginCtx.Done():
xl.Infof("VirtualNetPlugin reconnection delay interrupted for visitor [%s]", p.visitorCtx.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.visitorCtx.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.visitorCtx.Name)
p.controllerConn.Close()
p.controllerConn = nil
} else {
// xl.Debugf("No active controllerConn to cleanup for visitor [%s]", p.visitorCtx.Name)
}
// 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.visitorCtx.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.visitorCtx.Ctx) // Use base context for close logging
xl.Infof("Closing VirtualNetPlugin for visitor [%s]", p.visitorCtx.Name)
// 1. Signal the run loop goroutine to stop via context cancellation.
p.pluginCancel()
// 2. Unregister the route from the controller.
// This might implicitly cause the VnetController to close its end of the pipe (controllerConn).
if p.visitorCtx.VnetController != nil {
p.visitorCtx.VnetController.UnregisterClientRoute(p.visitorCtx.Name)
xl.Infof("Unregistered client route for visitor [%s]", p.visitorCtx.Name)
} else {
xl.Warnf("VnetController is nil during close for visitor [%s], cannot unregister route", p.visitorCtx.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.visitorCtx.Name)
return nil
}

94
pkg/vnet/README.md Normal file
View File

@ -0,0 +1,94 @@
# Virtual Network Controller for FRP
This package implements a virtual network controller that enables VPN-like functionality between FRP clients and servers.
## Overview
The Virtual Network Controller manages TUN devices and routing logic to create secure tunnels between clients and servers. It supports both client-side routing (based on destination IP) and server-side routing (based on source IP).
## How it Works
1. **Client Side**: Routes packets based on destination IP addresses. When a packet is destined for a network that matches a registered route, it's forwarded through the appropriate work connection.
2. **Server Side**: Routes packets based on source IP addresses. When a packet is received over a connection, its source IP is registered with that connection. Future packets from the TUN device with that source IP will be routed back through the same connection.
3. **TUN Device**: A virtual network interface that captures and injects packets at the IP layer.
## Usage
### Client Implementation
```go
// Set up VNet configuration
clientCfg := v1.VirtualNetConfig{
Address: "10.10.0.1/24",
Routes: []string{"10.20.0.0/24"}, // Routes to the server's network
}
// Create and initialize controller
controller := vnet.NewController(clientCfg)
if err := controller.Init(); err != nil {
// Handle error
}
// Parse route strings to IPNet objects
routes, err := vnet.ParseRoutes(clientCfg.Routes)
if err != nil {
// Handle error
}
// Register work connection with routes
if err := controller.RegisterClientRoute("server1", routes, workConn); err != nil {
// Handle error
}
// Start the controller (typically in a goroutine)
go func() {
if err := controller.Run(); err != nil {
// Handle error
}
}()
```
### Server Implementation
```go
// Set up VNet configuration
serverCfg := v1.VirtualNetConfig{
Address: "10.20.0.1/24",
}
// Create and initialize controller
controller := vnet.NewController(serverCfg)
if err := controller.Init(); err != nil {
// Handle error
}
// Register client work connection
if err := controller.RegisterServerConn("client1", workConn); err != nil {
// Handle error
}
// Start the controller (typically in a goroutine)
go func() {
if err := controller.Run(); err != nil {
// Handle error
}
}()
```
## Network Setup
For proper routing, you'll need to:
1. Configure the TUN device with appropriate IP addresses
2. Add routes to your routing table for traffic that should go through the VPN
3. Enable IP forwarding if needed
See the examples directory for a complete implementation example.
## Security Considerations
- The VNet controller operates at the IP layer, so all traffic is routed based on IP addresses
- Encryption should be handled at the transport layer (e.g., by the FRP connection)
- Consider using firewall rules to restrict access to the TUN device

365
pkg/vnet/controller.go Normal file
View File

@ -0,0 +1,365 @@
// 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/base64"
"fmt"
"io"
"net"
"sync"
"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/golib/log"
)
const (
maxPacketSize = 1350 // Maximum TUN packet size
)
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 {
conn, _, _, err := createTun(c.addr)
if err != nil {
return err
}
c.tun = conn
return nil
}
func (c *Controller) Run() error {
conn := c.tun
for {
buf := make([]byte, maxPacketSize)
n, err := conn.Read(buf)
if err != nil {
log.Warn("vnet read from tun error:", err)
return err
}
log.Trace("vnet read from tun [%d]: %s", n, base64.StdEncoding.EncodeToString(buf[:n]))
var src, dst net.IP
if waterutil.IsIPv4(buf[:n]) {
header, err := ipv4.ParseHeader(buf[:n])
if err != nil {
log.Warn(err)
continue
}
src = header.Src
dst = header.Dst
log.Infof("%s >> %s %d/%-4d %-4x %d",
header.Src, header.Dst,
header.Len, header.TotalLen, header.ID, header.Flags)
} else if waterutil.IsIPv6(buf[:n]) {
header, err := ipv6.ParseHeader(buf[:n])
if err != nil {
log.Warn(err)
continue
}
src = header.Src
dst = header.Dst
log.Infof("%s >> %s %d %d",
header.Src, header.Dst,
header.PayloadLen, header.TrafficClass)
} else {
log.Warnf("unknown packet, discarded(%d)", n)
continue
}
// 1. First try to route based on destination IP (client mode)
targetConn, err := c.clientRouter.findConn(dst)
if err == nil {
// Found matching destination route, sending data
if _, err := targetConn.Write(buf[:n]); err != nil {
log.Warn("write to client target conn error:", err)
}
continue
}
// 2. If client routing fails, try routing based on source IP (server mode)
targetConn, err = c.serverRouter.findConnBySrc(dst)
if err == nil {
// Found matching source route, sending data
if _, err := targetConn.Write(buf[:n]); err != nil {
log.Warn("write to server target conn error:", err)
}
continue
}
// 3. No matching route found
log.Warnf("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(conn io.ReadWriteCloser) {
defer func() {
// TODO
// unregister client route
}()
for {
// Read fixed size packet
buf := make([]byte, maxPacketSize)
// TODO: custom message format
n, err := conn.Read(buf)
if err != nil {
log.Warn("client read error:", err)
return
}
if n == 0 {
continue
}
if waterutil.IsIPv4(buf[:n]) {
header, err := ipv4.ParseHeader(buf[:n])
if err != nil {
log.Warn(err)
continue
}
log.Infof("%s >> %s %d/%-4d %-4x %d",
header.Src, header.Dst,
header.Len, header.TotalLen, header.ID, header.Flags)
} else if waterutil.IsIPv6(buf[:n]) {
header, err := ipv6.ParseHeader(buf[:n])
if err != nil {
log.Warn(err)
continue
}
log.Infof("%s >> %s %d %d",
header.Src, header.Dst,
header.PayloadLen, header.TrafficClass)
} else {
log.Warnf("unknown packet, discarded(%d)", n)
continue
}
// Write to TUN device
log.Trace("vnet write to tun (client) [%d]: %s", n, base64.StdEncoding.EncodeToString(buf[:n]))
_, err = c.tun.Write(buf[:n])
if err != nil {
log.Warn("client write tun error:", err)
return
}
}
}
// Server connection read loop
func (c *Controller) readLoopServer(conn io.ReadWriteCloser) {
for {
// Read fixed size packet
buf := make([]byte, maxPacketSize)
// TODO: custom message format
n, err := conn.Read(buf)
if err != nil {
log.Warn("server read error:", err)
return
}
if n == 0 {
continue
}
// Register source IP to connection mapping
if waterutil.IsIPv4(buf[:n]) || waterutil.IsIPv6(buf[:n]) {
var src net.IP
if waterutil.IsIPv4(buf[:n]) {
header, err := ipv4.ParseHeader(buf[:n])
if err == nil {
src = header.Src
c.serverRouter.registerSrcIP(src, conn)
}
} else if waterutil.IsIPv6(buf[:n]) {
header, err := ipv6.ParseHeader(buf[:n])
if err == nil {
src = header.Src
c.serverRouter.registerSrcIP(src, conn)
}
}
}
// Write to TUN
log.Trace("vnet write to tun (server) [%d]: %s", n, base64.StdEncoding.EncodeToString(buf[:n]))
_, err = c.tun.Write(buf[:n])
if err != nil {
log.Warn("server write tun error:", err)
return
}
}
}
// RegisterClientRoute Register client route (based on destination IP CIDR)
func (c *Controller) RegisterClientRoute(name string, routes []net.IPNet, conn io.ReadWriteCloser) error {
if err := c.clientRouter.addRoute(name, routes, conn); err != nil {
return err
}
go c.readLoopClient(conn)
return nil
}
// RegisterServerConn Register server connection (dynamically associates with source IPs)
func (c *Controller) RegisterServerConn(name string, conn io.ReadWriteCloser) error {
if err := c.serverRouter.addConn(name, conn); err != nil {
return err
}
go c.readLoopServer(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.ReadWriteCloser, 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.ReadWriteCloser // 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.ReadWriteCloser),
}
}
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.ReadWriteCloser, 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.ReadWriteCloser) {
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
}

View File

@ -0,0 +1,122 @@
// 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 main
import (
"flag"
"fmt"
"net"
"os"
"os/signal"
"syscall"
"time"
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/vnet"
)
func main() {
// Parse command-line flags
serverAddr := flag.String("server", "localhost:8000", "Server address (ip:port)")
clientTUNAddr := flag.String("tun", "10.10.0.1/24", "Client TUN device address")
serverTUNNet := flag.String("server-net", "10.20.0.0/24", "Server TUN network for routing")
flag.Parse()
fmt.Printf("Connecting to server at %s\n", *serverAddr)
fmt.Printf("Client TUN address: %s\n", *clientTUNAddr)
fmt.Printf("Server network route: %s\n", *serverTUNNet)
// Set up vnet controller on the client
clientCfg := v1.VirtualNetConfig{
Address: *clientTUNAddr,
Routes: []string{*serverTUNNet},
}
clientController := vnet.NewController(clientCfg)
if err := clientController.Init(); err != nil {
fmt.Printf("Client init error: %v\n", err)
return
}
// Handle shutdown gracefully
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-c
fmt.Println("\nShutting down client...")
os.Exit(0)
}()
// Start the controller
go func() {
if err := clientController.Run(); err != nil {
fmt.Printf("Client run error: %v\n", err)
}
}()
// Main connection loop
for {
// Connect to the server
conn, err := net.Dial("tcp", *serverAddr)
if err != nil {
fmt.Printf("Failed to connect to server %s: %v\n", *serverAddr, err)
time.Sleep(5 * time.Second)
continue
}
fmt.Printf("Connected to server at %s\n", conn.RemoteAddr())
// Wrap the connection
rwc := &readWriteCloserConn{conn}
// Parse routes from config
clientRoutes, err := vnet.ParseRoutes(clientCfg.Routes)
if err != nil {
fmt.Printf("Parse client routes error: %v\n", err)
conn.Close()
return
}
// Register the route with the connection
if err := clientController.RegisterClientRoute("server", clientRoutes, rwc); err != nil {
fmt.Printf("Register client route error: %v\n", err)
conn.Close()
time.Sleep(5 * time.Second)
continue
}
fmt.Println("VPN tunnel established with server")
fmt.Printf("Client TUN: %s, routes: %s\n", *clientTUNAddr, *serverTUNNet)
time.Sleep(60 * time.Second)
}
}
// readWriteCloserConn wraps net.Conn as io.ReadWriteCloser
type readWriteCloserConn struct {
net.Conn
}
func (rwc *readWriteCloserConn) Read(p []byte) (n int, err error) {
return rwc.Conn.Read(p)
}
func (rwc *readWriteCloserConn) Write(p []byte) (n int, err error) {
return rwc.Conn.Write(p)
}
func (rwc *readWriteCloserConn) Close() error {
return rwc.Conn.Close()
}

View File

@ -0,0 +1,114 @@
// 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 main
import (
"flag"
"fmt"
"net"
"os"
"os/signal"
"syscall"
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/vnet"
)
func main() {
// Parse command-line flags
listenAddr := flag.String("listen", ":8000", "Server listen address (ip:port)")
serverTUNAddr := flag.String("tun", "10.20.0.1/24", "Server TUN device address")
flag.Parse()
// Listen for incoming connections
listener, err := net.Listen("tcp", *listenAddr)
if err != nil {
fmt.Printf("Failed to listen on %s: %v\n", *listenAddr, err)
return
}
defer listener.Close()
fmt.Printf("Server listening on %s\n", *listenAddr)
fmt.Printf("Server TUN address: %s\n", *serverTUNAddr)
// Set up vnet controller on the server
serverCfg := v1.VirtualNetConfig{
Address: *serverTUNAddr, // Server TUN device address
}
serverController := vnet.NewController(serverCfg)
if err := serverController.Init(); err != nil {
fmt.Printf("Server init error: %v\n", err)
return
}
// Start the controller
go func() {
if err := serverController.Run(); err != nil {
fmt.Printf("Server run error: %v\n", err)
}
}()
// Handle shutdown gracefully
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-c
fmt.Println("\nShutting down server...")
os.Exit(0)
}()
fmt.Println("Waiting for client connection...")
// Accept and handle connections
for {
conn, err := listener.Accept()
if err != nil {
fmt.Printf("Error accepting connection: %v\n", err)
continue
}
fmt.Printf("New client connected from %s\n", conn.RemoteAddr())
// Wrap the connection
rwc := &readWriteCloserConn{conn}
// Register the connection
if err := serverController.RegisterServerConn("client", rwc); err != nil {
fmt.Printf("Register server connection error: %v\n", err)
conn.Close()
continue
}
fmt.Println("VPN tunnel established with client")
fmt.Printf("Server TUN: %s\n", *serverTUNAddr)
}
}
// readWriteCloserConn wraps net.Conn as io.ReadWriteCloser
type readWriteCloserConn struct {
net.Conn
}
func (rwc *readWriteCloserConn) Read(p []byte) (n int, err error) {
return rwc.Conn.Read(p)
}
func (rwc *readWriteCloserConn) Write(p []byte) (n int, err error) {
return rwc.Conn.Write(p)
}
func (rwc *readWriteCloserConn) Close() error {
return rwc.Conn.Close()
}

75
pkg/vnet/tun.go Normal file
View File

@ -0,0 +1,75 @@
// 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 (
"io"
"golang.zx2c4.com/wireguard/tun"
)
type tunDevice struct {
dev tun.Device
}
func (d *tunDevice) Read(p []byte) (int, error) {
// Wireguard's TUN implementation expects a 4-byte offset before the data
// Make a larger buffer with room for the offset
buf := make([]byte, len(p)+4)
// Create sizes array for wireguard to populate
sz := make([]int, 1)
// Call wireguard's Read with offset=4
n, err := d.dev.Read([][]byte{buf}, sz, 4)
if err != nil {
return 0, err
}
if n == 0 {
return 0, io.EOF
}
// Copy the actual data (excluding the 4-byte offset) to the output buffer
dataSize := sz[0]
if dataSize > len(p) {
dataSize = len(p)
}
copy(p, buf[4:4+dataSize])
return dataSize, nil
}
func (d *tunDevice) Write(p []byte) (int, error) {
buf := make([]byte, len(p)+4)
copy(buf[4:], p)
return d.dev.Write([][]byte{buf}, 4)
}
func (d *tunDevice) Close() error {
return d.dev.Close()
}
func createTunDevice(adviceName string, mtu int) (io.ReadWriteCloser, string, error) {
ifce, err := tun.CreateTUN(adviceName, mtu)
if err != nil {
return nil, "", err
}
name, err := ifce.Name()
if err != nil {
return nil, "", err
}
return &tunDevice{dev: ifce}, name, nil
}

84
pkg/vnet/tun_darwin.go Normal file
View File

@ -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 (
"fmt"
"io"
"net"
"os/exec"
"strings"
)
const (
defaultTunName = "utun"
defaultMTU = 1350
)
func createTun(addr string) (io.ReadWriteCloser, string, net.IP, error) {
ifce, name, err := createTunDevice(defaultTunName, defaultMTU)
if err != nil {
return nil, "", nil, err
}
ip, ipNet, err := net.ParseCIDR(addr)
if err != nil {
return nil, "", nil, err
}
// Calculate a peer IP for the point-to-point tunnel
peerIP := generatePeerIP(ip)
// Configure the interface with proper point-to-point addressing
cmd := fmt.Sprintf("ifconfig %s inet %s %s mtu %d up",
name, ip.String(), peerIP.String(), defaultMTU)
args := strings.Split(cmd, " ")
if err = exec.Command(args[0], args[1:]...).Run(); err != nil {
return nil, "", nil, err
}
// Add default route for the tunnel subnet
routes := []net.IPNet{*ipNet}
if err = addRoutes(name, routes); err != nil {
return nil, "", nil, err
}
return ifce, name, ip, 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 {
cmd := fmt.Sprintf("route add -net %s -interface %s", route.String(), ifName)
args := strings.Split(cmd, " ")
if err := exec.Command(args[0], args[1:]...).Run(); err != nil {
return err
}
}
return nil
}

80
pkg/vnet/tun_linux.go Normal file
View File

@ -0,0 +1,80 @@
// 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 (
"fmt"
"io"
"net"
"github.com/vishvananda/netlink"
)
const (
defaultTunName = "utun"
defaultMTU = 1350
)
func createTun(addr string) (io.ReadWriteCloser, string, net.IP, error) {
dev, name, err := createTunDevice(defaultTunName, defaultMTU)
if err != nil {
return nil, "", nil, err
}
ifce, err := net.InterfaceByName(name)
if err != nil {
return nil, "", nil, err
}
link, err := netlink.LinkByName(name)
if err != nil {
return nil, "", nil, err
}
ip, cidr, err := net.ParseCIDR(addr)
if err != nil {
return nil, "", nil, err
}
if err := netlink.AddrAdd(link, &netlink.Addr{
IPNet: &net.IPNet{
IP: ip,
Mask: cidr.Mask,
},
}); err != nil {
return nil, "", nil, err
}
if err := netlink.LinkSetUp(link); err != nil {
return nil, "", nil, err
}
if err = addRoutes(ifce, cidr); err != nil {
return nil, "", nil, err
}
return dev, name, ip, nil
}
func addRoutes(ifce *net.Interface, cidr *net.IPNet) error {
r := netlink.Route{
Dst: cidr,
}
if r.Gw == nil {
r.LinkIndex = ifce.Index
}
if err := netlink.RouteReplace(&r); err != nil {
return fmt.Errorf("add route %v %v: %v", r.Dst, r.Gw, err)
}
return nil
}