Compare commits

...

7 Commits

Author SHA1 Message Date
fatedier
ce366ee17f add proxy protocol support for UDP proxies (#4810) 2025-05-23 21:39:47 +08:00
fatedier
3128350dd6 feat: support YAML merge in strict configuration mode (#4809) 2025-05-23 19:25:34 +08:00
scientificworld
3be6efdd28 fix: type error in server_plugin doc (#4799) 2025-05-19 11:39:35 +08:00
CrynTox
6cbb26283c build: add x64 openbsd (#4780)
Co-authored-by: CrynTox <>
2025-05-06 12:11:20 +08:00
fatedier
75edea3370 update release notes (#4772) 2025-04-27 15:41:13 +08:00
fatedier
e687aef37e vnet: fix issues (#4771) 2025-04-27 15:22:28 +08:00
fatedier
a23455a737 update feature gates doc (#4755) 2025-04-17 01:34:14 +08:00
33 changed files with 728 additions and 228 deletions

View File

@@ -2,7 +2,7 @@ export PATH := $(PATH):`go env GOPATH`/bin
export GO111MODULE=on
LDFLAGS := -s -w
os-archs=darwin:amd64 darwin:arm64 freebsd:amd64 linux:amd64 linux:arm:7 linux:arm:5 linux:arm64 windows:amd64 windows:arm64 linux:mips64 linux:mips64le linux:mips:softfloat linux:mipsle:softfloat linux:riscv64 linux:loong64 android:arm64
os-archs=darwin:amd64 darwin:arm64 freebsd:amd64 openbsd:amd64 linux:amd64 linux:arm:7 linux:arm:5 linux:arm64 windows:amd64 windows:arm64 linux:mips64 linux:mips64le linux:mips:softfloat linux:mipsle:softfloat linux:riscv64 linux:loong64 android:arm64
all: build

View File

@@ -1025,7 +1025,7 @@ You can get user's real IP from HTTP request headers `X-Forwarded-For`.
#### Proxy Protocol
frp supports Proxy Protocol to send user's real IP to local services. It support all types except UDP.
frp supports Proxy Protocol to send user's real IP to local services.
Here is an example for https service:
@@ -1283,9 +1283,7 @@ frp supports feature gates to enable or disable experimental features. This allo
To enable an experimental feature, add the feature gate to your configuration:
```toml
featureGates = {
VirtualNet = true
}
featureGates = { VirtualNet = true }
```
### Feature Lifecycle

View File

@@ -1,8 +1,4 @@
### Notes
## Features
* **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
* **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.**
* Support for YAML merge functionality (anchors and references with dot-prefixed fields) in strict configuration mode without requiring `--strict-config=false` parameter.
* Support for proxy protocol in UDP proxies to preserve real client IP addresses.

View File

@@ -165,9 +165,9 @@ func (svr *Service) apiStatus(w http.ResponseWriter, _ *http.Request) {
res StatusResp = make(map[string][]ProxyStatusResp)
)
log.Infof("Http request [/api/status]")
log.Infof("http request [/api/status]")
defer func() {
log.Infof("Http response [/api/status]")
log.Infof("http response [/api/status]")
buf, _ = json.Marshal(&res)
_, _ = w.Write(buf)
}()
@@ -198,9 +198,9 @@ func (svr *Service) apiStatus(w http.ResponseWriter, _ *http.Request) {
func (svr *Service) apiGetConfig(w http.ResponseWriter, _ *http.Request) {
res := GeneralResponse{Code: 200}
log.Infof("Http get request [/api/config]")
log.Infof("http get request [/api/config]")
defer func() {
log.Infof("Http get response [/api/config], code [%d]", res.Code)
log.Infof("http get response [/api/config], code [%d]", res.Code)
w.WriteHeader(res.Code)
if len(res.Msg) > 0 {
_, _ = w.Write([]byte(res.Msg))
@@ -228,9 +228,9 @@ func (svr *Service) apiGetConfig(w http.ResponseWriter, _ *http.Request) {
func (svr *Service) apiPutConfig(w http.ResponseWriter, r *http.Request) {
res := GeneralResponse{Code: 200}
log.Infof("Http put request [/api/config]")
log.Infof("http put request [/api/config]")
defer func() {
log.Infof("Http put response [/api/config], code [%d]", res.Code)
log.Infof("http put response [/api/config], code [%d]", res.Code)
w.WriteHeader(res.Code)
if len(res.Msg) > 0 {
_, _ = w.Write([]byte(res.Msg))

View File

@@ -189,7 +189,7 @@ func (ctl *Control) handlePong(m msg.Message) {
inMsg := m.(*msg.Pong)
if inMsg.Error != "" {
xl.Errorf("Pong message contains error: %s", inMsg.Error)
xl.Errorf("pong message contains error: %s", inMsg.Error)
ctl.closeSession()
return
}

View File

@@ -20,13 +20,11 @@ import (
"net"
"reflect"
"strconv"
"strings"
"sync"
"time"
libio "github.com/fatedier/golib/io"
libnet "github.com/fatedier/golib/net"
pp "github.com/pires/go-proxyproto"
"golang.org/x/time/rate"
"github.com/fatedier/frp/pkg/config/types"
@@ -35,6 +33,7 @@ import (
plugin "github.com/fatedier/frp/pkg/plugin/client"
"github.com/fatedier/frp/pkg/transport"
"github.com/fatedier/frp/pkg/util/limit"
netpkg "github.com/fatedier/frp/pkg/util/net"
"github.com/fatedier/frp/pkg/util/xlog"
"github.com/fatedier/frp/pkg/vnet"
)
@@ -176,24 +175,9 @@ func (pxy *BaseProxy) HandleTCPWorkConnection(workConn net.Conn, m *msg.StartWor
}
if baseCfg.Transport.ProxyProtocolVersion != "" && m.SrcAddr != "" && m.SrcPort != 0 {
h := &pp.Header{
Command: pp.PROXY,
SourceAddr: connInfo.SrcAddr,
DestinationAddr: connInfo.DstAddr,
}
if strings.Contains(m.SrcAddr, ".") {
h.TransportProtocol = pp.TCPv4
} else {
h.TransportProtocol = pp.TCPv6
}
if baseCfg.Transport.ProxyProtocolVersion == "v1" {
h.Version = 1
} else if baseCfg.Transport.ProxyProtocolVersion == "v2" {
h.Version = 2
}
connInfo.ProxyProtocolHeader = h
// Use the common proxy protocol builder function
header := netpkg.BuildProxyProtocolHeaderStruct(connInfo.SrcAddr, connInfo.DstAddr, baseCfg.Transport.ProxyProtocolVersion)
connInfo.ProxyProtocolHeader = header
}
connInfo.Conn = remote
connInfo.UnderlyingConn = workConn

View File

@@ -205,5 +205,5 @@ func (pxy *SUDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) {
go workConnReaderFn(workConn, readCh)
go heartbeatFn(sendCh)
udp.Forwarder(pxy.localAddr, readCh, sendCh, int(pxy.clientCfg.UDPPacketSize))
udp.Forwarder(pxy.localAddr, readCh, sendCh, int(pxy.clientCfg.UDPPacketSize), pxy.cfg.Transport.ProxyProtocolVersion)
}

View File

@@ -171,5 +171,7 @@ func (pxy *UDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) {
go workConnSenderFn(pxy.workConn, pxy.sendCh)
go workConnReaderFn(pxy.workConn, pxy.readCh)
go heartbeatFn(pxy.sendCh)
udp.Forwarder(pxy.localAddr, pxy.readCh, pxy.sendCh, int(pxy.clientCfg.UDPPacketSize))
// Call Forwarder with proxy protocol version (empty string means no proxy protocol)
udp.Forwarder(pxy.localAddr, pxy.readCh, pxy.sendCh, int(pxy.clientCfg.UDPPacketSize), pxy.cfg.Transport.ProxyProtocolVersion)
}

View File

@@ -341,7 +341,7 @@ func (svr *Service) loopLoginUntilSuccess(maxInterval time.Duration, firstLoginE
ctl, err := NewControl(svr.ctx, sessionCtx)
if err != nil {
conn.Close()
xl.Errorf("NewControl error: %v", err)
xl.Errorf("new control error: %v", err)
return false, err
}
ctl.SetInWorkConnCallback(svr.handleWorkConnCb)

View File

@@ -121,7 +121,7 @@ Create new proxy
// http and https only
"custom_domains": []<string>,
"subdomain": <string>,
"locations": <string>,
"locations": []<string>,
"http_user": <string>,
"http_pwd": <string>,
"host_header_rewrite": <string>,

View File

@@ -49,9 +49,7 @@ type = "virtual_net"
# frpc.toml (client side)
serverAddr = "x.x.x.x"
serverPort = 7000
featureGates = {
VirtualNet = true
}
featureGates = { VirtualNet = true }
# Configure the virtual network interface
virtualNet.address = "100.86.0.2/24"

View File

@@ -17,7 +17,7 @@ make -f ./Makefile.cross-compiles
rm -rf ./release/packages
mkdir -p ./release/packages
os_all='linux windows darwin freebsd android'
os_all='linux windows darwin freebsd openbsd android'
arch_all='386 amd64 arm arm64 mips64 mips64le mips mipsle riscv64 loong64'
extra_all='_ hf'

View File

@@ -111,6 +111,33 @@ func LoadConfigureFromFile(path string, c any, strict bool) error {
return LoadConfigure(content, c, strict)
}
// parseYAMLWithDotFieldsHandling parses YAML with dot-prefixed fields handling
// This function handles both cases efficiently: with or without dot fields
func parseYAMLWithDotFieldsHandling(content []byte, target any) error {
var temp any
if err := yaml.Unmarshal(content, &temp); err != nil {
return err
}
// Remove dot fields if it's a map
if tempMap, ok := temp.(map[string]any); ok {
for key := range tempMap {
if strings.HasPrefix(key, ".") {
delete(tempMap, key)
}
}
}
// Convert to JSON and decode with strict validation
jsonBytes, err := json.Marshal(temp)
if err != nil {
return err
}
decoder := json.NewDecoder(bytes.NewReader(jsonBytes))
decoder.DisallowUnknownFields()
return decoder.Decode(target)
}
// LoadConfigure loads configuration from bytes and unmarshal into c.
// Now it supports json, yaml and toml format.
func LoadConfigure(b []byte, c any, strict bool) error {
@@ -134,10 +161,13 @@ func LoadConfigure(b []byte, c any, strict bool) error {
}
return decoder.Decode(c)
}
// It wasn't JSON. Unmarshal as YAML.
// Handle YAML content
if strict {
return yaml.UnmarshalStrict(b, c)
// In strict mode, always use our custom handler to support YAML merge
return parseYAMLWithDotFieldsHandling(b, c)
}
// Non-strict mode, parse normally
return yaml.Unmarshal(b, c)
}

View File

@@ -187,3 +187,122 @@ unixPath = "/tmp/uds.sock"
err = LoadConfigure([]byte(pluginStr), &clientCfg, true)
require.Error(err)
}
// TestYAMLMergeInStrictMode tests that YAML merge functionality works
// even in strict mode by properly handling dot-prefixed fields
func TestYAMLMergeInStrictMode(t *testing.T) {
require := require.New(t)
yamlContent := `
serverAddr: "127.0.0.1"
serverPort: 7000
.common: &common
type: stcp
secretKey: "test-secret"
localIP: 127.0.0.1
transport:
useEncryption: true
useCompression: true
proxies:
- name: ssh
localPort: 22
<<: *common
- name: web
localPort: 80
<<: *common
`
clientCfg := v1.ClientConfig{}
// This should work in strict mode
err := LoadConfigure([]byte(yamlContent), &clientCfg, true)
require.NoError(err)
// Verify the merge worked correctly
require.Equal("127.0.0.1", clientCfg.ServerAddr)
require.Equal(7000, clientCfg.ServerPort)
require.Len(clientCfg.Proxies, 2)
// Check first proxy
sshProxy := clientCfg.Proxies[0].ProxyConfigurer
require.Equal("ssh", sshProxy.GetBaseConfig().Name)
require.Equal("stcp", sshProxy.GetBaseConfig().Type)
// Check second proxy
webProxy := clientCfg.Proxies[1].ProxyConfigurer
require.Equal("web", webProxy.GetBaseConfig().Name)
require.Equal("stcp", webProxy.GetBaseConfig().Type)
}
// TestOptimizedYAMLProcessing tests the optimization logic for YAML processing
func TestOptimizedYAMLProcessing(t *testing.T) {
require := require.New(t)
yamlWithDotFields := []byte(`
serverAddr: "127.0.0.1"
.common: &common
type: stcp
proxies:
- name: test
<<: *common
`)
yamlWithoutDotFields := []byte(`
serverAddr: "127.0.0.1"
proxies:
- name: test
type: tcp
localPort: 22
`)
// Test that YAML without dot fields works in strict mode
clientCfg := v1.ClientConfig{}
err := LoadConfigure(yamlWithoutDotFields, &clientCfg, true)
require.NoError(err)
require.Equal("127.0.0.1", clientCfg.ServerAddr)
require.Len(clientCfg.Proxies, 1)
require.Equal("test", clientCfg.Proxies[0].ProxyConfigurer.GetBaseConfig().Name)
// Test that YAML with dot fields still works in strict mode
err = LoadConfigure(yamlWithDotFields, &clientCfg, true)
require.NoError(err)
require.Equal("127.0.0.1", clientCfg.ServerAddr)
require.Len(clientCfg.Proxies, 1)
require.Equal("test", clientCfg.Proxies[0].ProxyConfigurer.GetBaseConfig().Name)
require.Equal("stcp", clientCfg.Proxies[0].ProxyConfigurer.GetBaseConfig().Type)
}
// TestYAMLEdgeCases tests edge cases for YAML parsing, including non-map types
func TestYAMLEdgeCases(t *testing.T) {
require := require.New(t)
// Test array at root (should fail for frp config)
arrayYAML := []byte(`
- item1
- item2
`)
clientCfg := v1.ClientConfig{}
err := LoadConfigure(arrayYAML, &clientCfg, true)
require.Error(err) // Should fail because ClientConfig expects an object
// Test scalar at root (should fail for frp config)
scalarYAML := []byte(`"just a string"`)
err = LoadConfigure(scalarYAML, &clientCfg, true)
require.Error(err) // Should fail because ClientConfig expects an object
// Test empty object (should work)
emptyYAML := []byte(`{}`)
err = LoadConfigure(emptyYAML, &clientCfg, true)
require.NoError(err)
// Test nested structure without dots (should work)
nestedYAML := []byte(`
serverAddr: "127.0.0.1"
serverPort: 7000
`)
err = LoadConfigure(nestedYAML, &clientCfg, true)
require.NoError(err)
require.Equal("127.0.0.1", clientCfg.ServerAddr)
require.Equal(7000, clientCfg.ServerPort)
}

View File

@@ -18,9 +18,10 @@ package client
import (
"context"
"io"
"sync"
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/util/xlog"
)
func init() {
@@ -30,6 +31,8 @@ func init() {
type VirtualNetPlugin struct {
pluginCtx PluginContext
opts *v1.VirtualNetPluginOptions
mu sync.Mutex
conns map[io.ReadWriteCloser]struct{}
}
func NewVirtualNetPlugin(pluginCtx PluginContext, options v1.ClientPluginOptions) (Plugin, error) {
@@ -43,19 +46,32 @@ func NewVirtualNetPlugin(pluginCtx PluginContext, options v1.ClientPluginOptions
}
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
// Add the connection before starting the read loop to avoid race condition
// where RemoveConn might be called before the connection is added.
p.mu.Lock()
if p.conns == nil {
p.conns = make(map[io.ReadWriteCloser]struct{})
}
p.conns[connInfo.Conn] = struct{}{}
p.mu.Unlock()
// Register the connection with the controller and pass the cleanup function
p.pluginCtx.VnetController.StartServerConnReadLoop(ctx, connInfo.Conn, func() {
p.RemoveConn(connInfo.Conn)
})
}
func (p *VirtualNetPlugin) RemoveConn(conn io.ReadWriteCloser) {
p.mu.Lock()
defer p.mu.Unlock()
// Check if the map exists, as Close might have set it to nil concurrently
if p.conns != nil {
delete(p.conns, conn)
}
}
@@ -64,8 +80,13 @@ func (p *VirtualNetPlugin) Name() string {
}
func (p *VirtualNetPlugin) Close() error {
if p.pluginCtx.VnetController != nil {
p.pluginCtx.VnetController.UnregisterServerConn(p.pluginCtx.Name)
p.mu.Lock()
defer p.mu.Unlock()
// Close any remaining connections
for conn := range p.conns {
_ = conn.Close()
}
p.conns = nil
return nil
}

View File

@@ -60,7 +60,7 @@ func NewVirtualNetPlugin(pluginCtx PluginContext, options v1.VisitorPluginOption
return nil, errors.New("destinationIP is required")
}
// Parse DestinationIP as a single IP and create a host route
// Parse DestinationIP and create a host route.
ip := net.ParseIP(opts.DestinationIP)
if ip == nil {
return nil, fmt.Errorf("invalid destination IP address [%s]", opts.DestinationIP)
@@ -91,7 +91,7 @@ func (p *VirtualNetPlugin) Start() {
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)
xl.Infof("starting VirtualNetPlugin for visitor [%s], attempting to register routes for %s", p.pluginCtx.Name, routeStr)
go p.run()
}
@@ -101,10 +101,8 @@ func (p *VirtualNetPlugin) run() {
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()
@@ -112,7 +110,6 @@ func (p *VirtualNetPlugin) run() {
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:
@@ -120,65 +117,43 @@ func (p *VirtualNetPlugin) run() {
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
close(currentCloseSignal) // Signal the run loop on close.
})
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)
xl.Infof("attempting to register client route for visitor [%s]", p.pluginCtx.Name)
p.pluginCtx.VnetController.RegisterClientRoute(p.ctx, p.pluginCtx.Name, p.routes, controllerConn)
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.
// Wait for context cancellation or connection close.
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.
xl.Infof("detected connection closed via CloseNotifyConn for visitor [%s].", p.pluginCtx.Name)
// HandleConn closed the plugin side. 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)
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
return
}
// 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)
xl.Infof("re-establishing virtual connection for visitor [%s]...", p.pluginCtx.Name)
}
}
@@ -187,46 +162,31 @@ 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)
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)
xl := xlog.FromContextSafe(p.pluginCtx.Ctx)
xl.Infof("closing VirtualNetPlugin for visitor [%s]", p.pluginCtx.Name)
// 1. Signal the run loop goroutine to stop via context cancellation.
// Signal the run loop goroutine to stop.
p.cancel()
// 2. Unregister the route from the controller.
// This might implicitly cause the VnetController to close its end of the pipe (controllerConn).
// Unregister the route from the controller.
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)
xl.Infof("unregistered client route for visitor [%s]", p.pluginCtx.Name)
}
// 3. Explicitly close the controller side of the pipe managed by this plugin.
// Explicitly close the controller side of the pipe.
// 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)
xl.Infof("finished cleaning up connections during close for visitor [%s]", p.pluginCtx.Name)
return nil
}

View File

@@ -24,6 +24,7 @@ import (
"github.com/fatedier/golib/pool"
"github.com/fatedier/frp/pkg/msg"
netpkg "github.com/fatedier/frp/pkg/util/net"
)
func NewUDPPacket(buf []byte, laddr, raddr *net.UDPAddr) *msg.UDPPacket {
@@ -69,7 +70,7 @@ func ForwardUserConn(udpConn *net.UDPConn, readCh <-chan *msg.UDPPacket, sendCh
}
}
func Forwarder(dstAddr *net.UDPAddr, readCh <-chan *msg.UDPPacket, sendCh chan<- msg.Message, bufSize int) {
func Forwarder(dstAddr *net.UDPAddr, readCh <-chan *msg.UDPPacket, sendCh chan<- msg.Message, bufSize int, proxyProtocolVersion string) {
var mu sync.RWMutex
udpConnMap := make(map[string]*net.UDPConn)
@@ -110,6 +111,7 @@ func Forwarder(dstAddr *net.UDPAddr, readCh <-chan *msg.UDPPacket, sendCh chan<-
if err != nil {
continue
}
mu.Lock()
udpConn, ok := udpConnMap[udpMsg.RemoteAddr.String()]
if !ok {
@@ -122,6 +124,18 @@ func Forwarder(dstAddr *net.UDPAddr, readCh <-chan *msg.UDPPacket, sendCh chan<-
}
mu.Unlock()
// Add proxy protocol header if configured
if proxyProtocolVersion != "" && udpMsg.RemoteAddr != nil {
ppBuf, err := netpkg.BuildProxyProtocolHeader(udpMsg.RemoteAddr, dstAddr, proxyProtocolVersion)
if err == nil {
// Prepend proxy protocol header to the UDP payload
finalBuf := make([]byte, len(ppBuf)+len(buf))
copy(finalBuf, ppBuf)
copy(finalBuf[len(ppBuf):], buf)
buf = finalBuf
}
}
_, err = udpConn.Write(buf)
if err != nil {
udpConn.Close()

View File

@@ -0,0 +1,45 @@
// 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 net
import (
"bytes"
"fmt"
"net"
pp "github.com/pires/go-proxyproto"
)
func BuildProxyProtocolHeaderStruct(srcAddr, dstAddr net.Addr, version string) *pp.Header {
var versionByte byte
if version == "v1" {
versionByte = 1
} else {
versionByte = 2 // default to v2
}
return pp.HeaderProxyFromAddrs(versionByte, srcAddr, dstAddr)
}
func BuildProxyProtocolHeader(srcAddr, dstAddr net.Addr, version string) ([]byte, error) {
h := BuildProxyProtocolHeaderStruct(srcAddr, dstAddr, version)
// Convert header to bytes using a buffer
var buf bytes.Buffer
_, err := h.WriteTo(&buf)
if err != nil {
return nil, fmt.Errorf("failed to write proxy protocol header: %v", err)
}
return buf.Bytes(), nil
}

View File

@@ -0,0 +1,178 @@
package net
import (
"net"
"testing"
pp "github.com/pires/go-proxyproto"
"github.com/stretchr/testify/require"
)
func TestBuildProxyProtocolHeader(t *testing.T) {
require := require.New(t)
tests := []struct {
name string
srcAddr net.Addr
dstAddr net.Addr
version string
expectError bool
}{
{
name: "UDP IPv4 v2",
srcAddr: &net.UDPAddr{IP: net.ParseIP("192.168.1.100"), Port: 12345},
dstAddr: &net.UDPAddr{IP: net.ParseIP("10.0.0.1"), Port: 3306},
version: "v2",
expectError: false,
},
{
name: "TCP IPv4 v1",
srcAddr: &net.TCPAddr{IP: net.ParseIP("192.168.1.100"), Port: 12345},
dstAddr: &net.TCPAddr{IP: net.ParseIP("10.0.0.1"), Port: 80},
version: "v1",
expectError: false,
},
{
name: "UDP IPv6 v2",
srcAddr: &net.UDPAddr{IP: net.ParseIP("2001:db8::1"), Port: 12345},
dstAddr: &net.UDPAddr{IP: net.ParseIP("::1"), Port: 3306},
version: "v2",
expectError: false,
},
{
name: "TCP IPv6 v1",
srcAddr: &net.TCPAddr{IP: net.ParseIP("::1"), Port: 12345},
dstAddr: &net.TCPAddr{IP: net.ParseIP("2001:db8::1"), Port: 80},
version: "v1",
expectError: false,
},
{
name: "nil source address",
srcAddr: nil,
dstAddr: &net.UDPAddr{IP: net.ParseIP("10.0.0.1"), Port: 3306},
version: "v2",
expectError: false,
},
{
name: "nil destination address",
srcAddr: &net.TCPAddr{IP: net.ParseIP("192.168.1.100"), Port: 12345},
dstAddr: nil,
version: "v2",
expectError: false,
},
{
name: "unsupported address type",
srcAddr: &net.UnixAddr{Name: "/tmp/test.sock", Net: "unix"},
dstAddr: &net.UDPAddr{IP: net.ParseIP("10.0.0.1"), Port: 3306},
version: "v2",
expectError: false,
},
}
for _, tt := range tests {
header, err := BuildProxyProtocolHeader(tt.srcAddr, tt.dstAddr, tt.version)
if tt.expectError {
require.Error(err, "test case: %s", tt.name)
continue
}
require.NoError(err, "test case: %s", tt.name)
require.NotEmpty(header, "test case: %s", tt.name)
}
}
func TestBuildProxyProtocolHeaderStruct(t *testing.T) {
require := require.New(t)
tests := []struct {
name string
srcAddr net.Addr
dstAddr net.Addr
version string
expectedProtocol pp.AddressFamilyAndProtocol
expectedVersion byte
expectedCommand pp.ProtocolVersionAndCommand
expectedSourceAddr net.Addr
expectedDestAddr net.Addr
}{
{
name: "TCP IPv4 v2",
srcAddr: &net.TCPAddr{IP: net.ParseIP("192.168.1.100"), Port: 12345},
dstAddr: &net.TCPAddr{IP: net.ParseIP("10.0.0.1"), Port: 80},
version: "v2",
expectedProtocol: pp.TCPv4,
expectedVersion: 2,
expectedCommand: pp.PROXY,
expectedSourceAddr: &net.TCPAddr{IP: net.ParseIP("192.168.1.100"), Port: 12345},
expectedDestAddr: &net.TCPAddr{IP: net.ParseIP("10.0.0.1"), Port: 80},
},
{
name: "UDP IPv6 v1",
srcAddr: &net.UDPAddr{IP: net.ParseIP("2001:db8::1"), Port: 12345},
dstAddr: &net.UDPAddr{IP: net.ParseIP("::1"), Port: 3306},
version: "v1",
expectedProtocol: pp.UDPv6,
expectedVersion: 1,
expectedCommand: pp.PROXY,
expectedSourceAddr: &net.UDPAddr{IP: net.ParseIP("2001:db8::1"), Port: 12345},
expectedDestAddr: &net.UDPAddr{IP: net.ParseIP("::1"), Port: 3306},
},
{
name: "TCP IPv6 default version",
srcAddr: &net.TCPAddr{IP: net.ParseIP("::1"), Port: 12345},
dstAddr: &net.TCPAddr{IP: net.ParseIP("2001:db8::1"), Port: 80},
version: "",
expectedProtocol: pp.TCPv6,
expectedVersion: 2, // default to v2
expectedCommand: pp.PROXY,
expectedSourceAddr: &net.TCPAddr{IP: net.ParseIP("::1"), Port: 12345},
expectedDestAddr: &net.TCPAddr{IP: net.ParseIP("2001:db8::1"), Port: 80},
},
{
name: "nil source address",
srcAddr: nil,
dstAddr: &net.UDPAddr{IP: net.ParseIP("10.0.0.1"), Port: 3306},
version: "v2",
expectedProtocol: pp.UNSPEC,
expectedVersion: 2,
expectedCommand: pp.LOCAL,
expectedSourceAddr: nil, // go-proxyproto sets both to nil when srcAddr is nil
expectedDestAddr: nil,
},
{
name: "nil destination address",
srcAddr: &net.TCPAddr{IP: net.ParseIP("192.168.1.100"), Port: 12345},
dstAddr: nil,
version: "v2",
expectedProtocol: pp.UNSPEC,
expectedVersion: 2,
expectedCommand: pp.LOCAL,
expectedSourceAddr: nil, // go-proxyproto sets both to nil when dstAddr is nil
expectedDestAddr: nil,
},
{
name: "unsupported address type",
srcAddr: &net.UnixAddr{Name: "/tmp/test.sock", Net: "unix"},
dstAddr: &net.UDPAddr{IP: net.ParseIP("10.0.0.1"), Port: 3306},
version: "v2",
expectedProtocol: pp.UNSPEC,
expectedVersion: 2,
expectedCommand: pp.LOCAL,
expectedSourceAddr: nil, // go-proxyproto sets both to nil for unsupported types
expectedDestAddr: nil,
},
}
for _, tt := range tests {
header := BuildProxyProtocolHeaderStruct(tt.srcAddr, tt.dstAddr, tt.version)
require.NotNil(header, "test case: %s", tt.name)
require.Equal(tt.expectedCommand, header.Command, "test case: %s", tt.name)
require.Equal(tt.expectedSourceAddr, header.SourceAddr, "test case: %s", tt.name)
require.Equal(tt.expectedDestAddr, header.DestinationAddr, "test case: %s", tt.name)
require.Equal(tt.expectedProtocol, header.TransportProtocol, "test case: %s", tt.name)
require.Equal(tt.expectedVersion, header.Version, "test case: %s", tt.name)
}
}

View File

@@ -14,7 +14,7 @@
package version
var version = "0.62.0"
var version = "0.63.0"
func Full() string {
return version

View File

@@ -162,7 +162,7 @@ func (rp *HTTPReverseProxy) UnRegister(routeCfg RouteConfig) {
func (rp *HTTPReverseProxy) GetRouteConfig(domain, location, routeByHTTPUser string) *RouteConfig {
vr, ok := rp.getVhost(domain, location, routeByHTTPUser)
if ok {
log.Debugf("get new HTTP request host [%s] path [%s] httpuser [%s]", domain, location, routeByHTTPUser)
log.Debugf("get new http request host [%s] path [%s] httpuser [%s]", domain, location, routeByHTTPUser)
return vr.payload.(*RouteConfig)
}
return nil

View File

@@ -275,7 +275,7 @@ func (l *Listener) Accept() (net.Conn, error) {
xl := xlog.FromContextSafe(l.ctx)
conn, ok := <-l.accept
if !ok {
return nil, fmt.Errorf("Listener closed")
return nil, fmt.Errorf("listener closed")
}
// if rewriteHost func is exist

View File

@@ -87,7 +87,7 @@ func (c *Controller) handlePacket(buf []byte) {
case waterutil.IsIPv4(buf):
header, err := ipv4.ParseHeader(buf)
if err != nil {
log.Warnf("parse ipv4 header error:", err)
log.Warnf("parse ipv4 header error: %v", err)
return
}
src = header.Src
@@ -98,7 +98,7 @@ func (c *Controller) handlePacket(buf []byte) {
case waterutil.IsIPv6(buf):
header, err := ipv6.ParseHeader(buf)
if err != nil {
log.Warnf("parse ipv6 header error:", err)
log.Warnf("parse ipv6 header error: %v", err)
return
}
src = header.Src
@@ -137,6 +137,12 @@ func (c *Controller) Stop() error {
// Client connection read loop
func (c *Controller) readLoopClient(ctx context.Context, conn io.ReadWriteCloser) {
xl := xlog.FromContextSafe(ctx)
defer func() {
// Remove the route when read loop ends (connection closed)
c.clientRouter.removeConnRoute(conn)
conn.Close()
}()
for {
data, err := ReadMessage(conn)
if err != nil {
@@ -181,8 +187,18 @@ func (c *Controller) readLoopClient(ctx context.Context, conn io.ReadWriteCloser
}
// Server connection read loop
func (c *Controller) readLoopServer(ctx context.Context, conn io.ReadWriteCloser) {
func (c *Controller) readLoopServer(ctx context.Context, conn io.ReadWriteCloser, onClose func()) {
xl := xlog.FromContextSafe(ctx)
defer func() {
// Clean up all IP mappings associated with this connection when it closes
c.serverRouter.cleanupConnIPs(conn)
// Call the provided callback upon closure
if onClose != nil {
onClose()
}
conn.Close()
}()
for {
data, err := ReadMessage(conn)
if err != nil {
@@ -220,27 +236,11 @@ func (c *Controller) readLoopServer(ctx context.Context, conn io.ReadWriteCloser
}
}
// 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
}
// RegisterClientRoute registers a client route (based on destination IP CIDR)
// and starts the read loop
func (c *Controller) RegisterClientRoute(ctx context.Context, name string, routes []net.IPNet, conn io.ReadWriteCloser) {
c.clientRouter.addRoute(name, routes, conn)
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
@@ -248,6 +248,12 @@ func (c *Controller) UnregisterClientRoute(name string) {
c.clientRouter.delRoute(name)
}
// StartServerConnReadLoop starts the read loop for a server connection
// (dynamically associates with source IPs)
func (c *Controller) StartServerConnReadLoop(ctx context.Context, conn io.ReadWriteCloser, onClose func()) {
go c.readLoopServer(ctx, conn, onClose)
}
// ParseRoutes Convert route strings to IPNet objects
func ParseRoutes(routeStrings []string) ([]net.IPNet, error) {
routes := make([]net.IPNet, 0, len(routeStrings))
@@ -273,7 +279,7 @@ func newClientRouter() *clientRouter {
}
}
func (r *clientRouter) addRoute(name string, routes []net.IPNet, conn io.ReadWriteCloser) error {
func (r *clientRouter) addRoute(name string, routes []net.IPNet, conn io.ReadWriteCloser) {
r.mu.Lock()
defer r.mu.Unlock()
r.routes[name] = &routeElement{
@@ -281,7 +287,6 @@ func (r *clientRouter) addRoute(name string, routes []net.IPNet, conn io.ReadWri
routes: routes,
conn: conn,
}
return nil
}
func (r *clientRouter) findConn(dst net.IP) (io.Writer, error) {
@@ -303,32 +308,29 @@ func (r *clientRouter) delRoute(name string) {
delete(r.routes, name)
}
// Server router (based on source IP routing)
func (r *clientRouter) removeConnRoute(conn io.Writer) {
r.mu.Lock()
defer r.mu.Unlock()
for name, re := range r.routes {
if re.conn == conn {
delete(r.routes, name)
return
}
}
}
// Server router (based solely 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
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()
@@ -340,17 +342,41 @@ func (r *serverRouter) findConnBySrc(src net.IP) (io.Writer, error) {
}
func (r *serverRouter) registerSrcIP(src net.IP, conn io.Writer) {
key := src.String()
r.mu.RLock()
existingConn, ok := r.srcIPConns[key]
r.mu.RUnlock()
// If the entry exists and the connection is the same, no need to do anything.
if ok && existingConn == conn {
return
}
// Acquire write lock to update the map.
r.mu.Lock()
defer r.mu.Unlock()
r.srcIPConns[src.String()] = conn
// Double-check after acquiring the write lock to handle potential race conditions.
existingConn, ok = r.srcIPConns[key]
if ok && existingConn == conn {
return
}
r.srcIPConns[key] = conn
}
func (r *serverRouter) delConn(name string) {
// cleanupConnIPs removes all IP mappings associated with the specified connection
func (r *serverRouter) cleanupConnIPs(conn io.Writer) {
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
// Find and delete all IP mappings pointing to this connection
for ip, mappedConn := range r.srcIPConns {
if mappedConn == conn {
delete(r.srcIPConns, ip)
}
}
}
type routeElement struct {

View File

@@ -33,7 +33,7 @@ func ReadMessage(r io.Reader) ([]byte, error) {
var length uint32
err := binary.Read(r, binary.LittleEndian, &length)
if err != nil {
return nil, fmt.Errorf("read message length error: %v", err)
return nil, fmt.Errorf("read message length error: %w", err)
}
// Check length to prevent DoS
@@ -48,7 +48,7 @@ func ReadMessage(r io.Reader) ([]byte, error) {
data := make([]byte, length)
_, err = io.ReadFull(r, data)
if err != nil {
return nil, fmt.Errorf("read message data error: %v", err)
return nil, fmt.Errorf("read message data error: %w", err)
}
return data, nil
@@ -68,13 +68,13 @@ func WriteMessage(w io.Writer, data []byte) error {
// Write length
err := binary.Write(w, binary.LittleEndian, length)
if err != nil {
return fmt.Errorf("write message length error: %v", err)
return fmt.Errorf("write message length error: %w", err)
}
// Write message data
_, err = w.Write(data)
if err != nil {
return fmt.Errorf("write message data error: %v", err)
return fmt.Errorf("write message data error: %w", err)
}
return nil

View File

@@ -23,7 +23,8 @@ import (
)
const (
offset = 16
offset = 16
defaultPacketSize = 1420
)
type TunDevice interface {
@@ -35,20 +36,45 @@ func OpenTun(ctx context.Context, addr string) (TunDevice, error) {
if err != nil {
return nil, err
}
return &tunDeviceWrapper{dev: td}, nil
mtu, err := td.MTU()
if err != nil {
mtu = defaultPacketSize
}
bufferSize := max(mtu, defaultPacketSize)
batchSize := td.BatchSize()
device := &tunDeviceWrapper{
dev: td,
bufferSize: bufferSize,
readBuffers: make([][]byte, batchSize),
sizeBuffer: make([]int, batchSize),
}
for i := range device.readBuffers {
device.readBuffers[i] = make([]byte, offset+bufferSize)
}
return device, nil
}
type tunDeviceWrapper struct {
dev tun.Device
dev tun.Device
bufferSize int
readBuffers [][]byte
packetBuffers [][]byte
sizeBuffer []int
}
func (d *tunDeviceWrapper) Read(p []byte) (int, error) {
buf := pool.GetBuf(len(p) + offset)
defer pool.PutBuf(buf)
if len(d.packetBuffers) > 0 {
n := copy(p, d.packetBuffers[0])
d.packetBuffers = d.packetBuffers[1:]
return n, nil
}
sz := make([]int, 1)
n, err := d.dev.Read([][]byte{buf}, sz, offset)
n, err := d.dev.Read(d.readBuffers, d.sizeBuffer, offset)
if err != nil {
return 0, err
}
@@ -56,20 +82,26 @@ func (d *tunDeviceWrapper) Read(p []byte) (int, error) {
return 0, io.EOF
}
dataSize := sz[0]
if dataSize > len(p) {
dataSize = len(p)
for i := range n {
if d.sizeBuffer[i] <= 0 {
continue
}
d.packetBuffers = append(d.packetBuffers, d.readBuffers[i][offset:offset+d.sizeBuffer[i]])
}
copy(p, buf[offset:offset+dataSize])
dataSize := copy(p, d.packetBuffers[0])
d.packetBuffers = d.packetBuffers[1:]
return dataSize, nil
}
func (d *tunDeviceWrapper) Write(p []byte) (int, error) {
buf := pool.GetBuf(len(p) + offset)
buf := pool.GetBuf(offset + d.bufferSize)
defer pool.PutBuf(buf)
copy(buf[offset:], p)
return d.dev.Write([][]byte{buf}, offset)
n := copy(buf[offset:], p)
_, err := d.dev.Write([][]byte{buf[:offset+n]}, offset)
return n, err
}
func (d *tunDeviceWrapper) Close() error {

View File

@@ -16,35 +16,44 @@ package vnet
import (
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"net"
"strconv"
"strings"
"github.com/vishvananda/netlink"
"golang.zx2c4.com/wireguard/tun"
)
const (
defaultTunName = "utun"
defaultMTU = 1420
baseTunName = "utun"
defaultMTU = 1420
)
func openTun(_ context.Context, addr string) (tun.Device, error) {
dev, err := tun.CreateTUN(defaultTunName, defaultMTU)
name, err := findNextTunName(baseTunName)
if err != nil {
name = getFallbackTunName(baseTunName, addr)
}
tunDevice, err := tun.CreateTUN(name, defaultMTU)
if err != nil {
return nil, fmt.Errorf("failed to create TUN device '%s': %w", name, err)
}
actualName, err := tunDevice.Name()
if err != nil {
return nil, err
}
name, err := dev.Name()
ifn, err := net.InterfaceByName(actualName)
if err != nil {
return nil, err
}
ifn, err := net.InterfaceByName(name)
if err != nil {
return nil, err
}
link, err := netlink.LinkByName(name)
link, err := netlink.LinkByName(actualName)
if err != nil {
return nil, err
}
@@ -69,7 +78,34 @@ func openTun(_ context.Context, addr string) (tun.Device, error) {
if err = addRoutes(ifn, cidr); err != nil {
return nil, err
}
return dev, nil
return tunDevice, nil
}
func findNextTunName(basename string) (string, error) {
interfaces, err := net.Interfaces()
if err != nil {
return "", fmt.Errorf("failed to get network interfaces: %w", err)
}
maxSuffix := -1
for _, iface := range interfaces {
name := iface.Name
if strings.HasPrefix(name, basename) {
suffix := name[len(basename):]
if suffix == "" {
continue
}
numSuffix, err := strconv.Atoi(suffix)
if err == nil && numSuffix > maxSuffix {
maxSuffix = numSuffix
}
}
}
nextSuffix := maxSuffix + 1
name := fmt.Sprintf("%s%d", basename, nextSuffix)
return name, nil
}
func addRoutes(ifn *net.Interface, cidr *net.IPNet) error {
@@ -82,3 +118,14 @@ func addRoutes(ifn *net.Interface, cidr *net.IPNet) error {
}
return nil
}
// getFallbackTunName generates a deterministic fallback TUN device name
// based on the base name and the provided address string using a hash.
func getFallbackTunName(baseName, addr string) string {
hasher := sha256.New()
hasher.Write([]byte(addr))
hashBytes := hasher.Sum(nil)
// Use first 4 bytes -> 8 hex chars for brevity, respecting IFNAMSIZ limit.
shortHash := hex.EncodeToString(hashBytes[:4])
return fmt.Sprintf("%s%s", baseName, shortHash)
}

View File

@@ -224,7 +224,7 @@ func (ctl *Control) Close() error {
func (ctl *Control) Replaced(newCtl *Control) {
xl := ctl.xl
xl.Infof("Replaced by client [%s]", newCtl.runID)
xl.Infof("replaced by client [%s]", newCtl.runID)
ctl.runID = ""
ctl.conn.Close()
}

View File

@@ -97,14 +97,14 @@ func (svr *Service) healthz(w http.ResponseWriter, _ *http.Request) {
func (svr *Service) apiServerInfo(w http.ResponseWriter, r *http.Request) {
res := GeneralResponse{Code: 200}
defer func() {
log.Infof("Http response [%s]: code [%d]", r.URL.Path, res.Code)
log.Infof("http response [%s]: code [%d]", r.URL.Path, res.Code)
w.WriteHeader(res.Code)
if len(res.Msg) > 0 {
_, _ = w.Write([]byte(res.Msg))
}
}()
log.Infof("Http request: [%s]", r.URL.Path)
log.Infof("http request: [%s]", r.URL.Path)
serverStats := mem.StatsCollector.GetServer()
svrResp := serverInfoResp{
Version: version.Full(),
@@ -218,13 +218,13 @@ func (svr *Service) apiProxyByType(w http.ResponseWriter, r *http.Request) {
proxyType := params["type"]
defer func() {
log.Infof("Http response [%s]: code [%d]", r.URL.Path, res.Code)
log.Infof("http response [%s]: code [%d]", r.URL.Path, res.Code)
w.WriteHeader(res.Code)
if len(res.Msg) > 0 {
_, _ = w.Write([]byte(res.Msg))
}
}()
log.Infof("Http request: [%s]", r.URL.Path)
log.Infof("http request: [%s]", r.URL.Path)
proxyInfoResp := GetProxyInfoResp{}
proxyInfoResp.Proxies = svr.getProxyStatsByType(proxyType)
@@ -290,13 +290,13 @@ func (svr *Service) apiProxyByTypeAndName(w http.ResponseWriter, r *http.Request
name := params["name"]
defer func() {
log.Infof("Http response [%s]: code [%d]", r.URL.Path, res.Code)
log.Infof("http response [%s]: code [%d]", r.URL.Path, res.Code)
w.WriteHeader(res.Code)
if len(res.Msg) > 0 {
_, _ = w.Write([]byte(res.Msg))
}
}()
log.Infof("Http request: [%s]", r.URL.Path)
log.Infof("http request: [%s]", r.URL.Path)
var proxyStatsResp GetProxyStatsResp
proxyStatsResp, res.Code, res.Msg = svr.getProxyStatsByTypeAndName(proxyType, name)
@@ -358,13 +358,13 @@ func (svr *Service) apiProxyTraffic(w http.ResponseWriter, r *http.Request) {
name := params["name"]
defer func() {
log.Infof("Http response [%s]: code [%d]", r.URL.Path, res.Code)
log.Infof("http response [%s]: code [%d]", r.URL.Path, res.Code)
w.WriteHeader(res.Code)
if len(res.Msg) > 0 {
_, _ = w.Write([]byte(res.Msg))
}
}()
log.Infof("Http request: [%s]", r.URL.Path)
log.Infof("http request: [%s]", r.URL.Path)
trafficResp := GetProxyTrafficResp{}
trafficResp.Name = name
@@ -386,9 +386,9 @@ func (svr *Service) apiProxyTraffic(w http.ResponseWriter, r *http.Request) {
func (svr *Service) deleteProxies(w http.ResponseWriter, r *http.Request) {
res := GeneralResponse{Code: 200}
log.Infof("Http request: [%s]", r.URL.Path)
log.Infof("http request: [%s]", r.URL.Path)
defer func() {
log.Infof("Http response [%s]: code [%d]", r.URL.Path, res.Code)
log.Infof("http response [%s]: code [%d]", r.URL.Path, res.Code)
w.WriteHeader(res.Code)
if len(res.Msg) > 0 {
_, _ = w.Write([]byte(res.Msg))

View File

@@ -427,7 +427,7 @@ func (svr *Service) handleConnection(ctx context.Context, conn net.Conn, interna
_ = conn.SetReadDeadline(time.Now().Add(connReadTimeout))
if rawMsg, err = msg.ReadMsg(conn); err != nil {
log.Tracef("Failed to read message: %v", err)
log.Tracef("failed to read message: %v", err)
conn.Close()
return
}
@@ -475,7 +475,7 @@ func (svr *Service) handleConnection(ctx context.Context, conn net.Conn, interna
})
}
default:
log.Warnf("Error message type for the new connection [%s]", conn.RemoteAddr().String())
log.Warnf("error message type for the new connection [%s]", conn.RemoteAddr().String())
conn.Close()
}
}
@@ -488,7 +488,7 @@ func (svr *Service) HandleListener(l net.Listener, internal bool) {
for {
c, err := l.Accept()
if err != nil {
log.Warnf("Listener for incoming connections from client closed")
log.Warnf("listener for incoming connections from client closed")
return
}
// inject xlog object into net.Conn context
@@ -504,7 +504,7 @@ func (svr *Service) HandleListener(l net.Listener, internal bool) {
var isTLS, custom bool
c, isTLS, custom, err = netpkg.CheckAndEnableTLSServerConnWithTimeout(c, svr.tlsConfig, forceTLS, connReadTimeout)
if err != nil {
log.Warnf("CheckAndEnableTLSServerConnWithTimeout error: %v", err)
log.Warnf("checkAndEnableTLSServerConnWithTimeout error: %v", err)
originConn.Close()
continue
}
@@ -520,7 +520,7 @@ func (svr *Service) HandleListener(l net.Listener, internal bool) {
fmuxCfg.MaxStreamWindowSize = 6 * 1024 * 1024
session, err := fmux.Server(frpConn, fmuxCfg)
if err != nil {
log.Warnf("Failed to create mux connection: %v", err)
log.Warnf("failed to create mux connection: %v", err)
frpConn.Close()
return
}
@@ -528,7 +528,7 @@ func (svr *Service) HandleListener(l net.Listener, internal bool) {
for {
stream, err := session.AcceptStream()
if err != nil {
log.Debugf("Accept new mux stream error: %v", err)
log.Debugf("accept new mux stream error: %v", err)
session.Close()
return
}
@@ -546,7 +546,7 @@ func (svr *Service) HandleQUICListener(l *quic.Listener) {
for {
c, err := l.Accept(context.Background())
if err != nil {
log.Warnf("QUICListener for incoming connections from client closed")
log.Warnf("quic listener for incoming connections from client closed")
return
}
// Start a new goroutine to handle connection.
@@ -554,7 +554,7 @@ func (svr *Service) HandleQUICListener(l *quic.Listener) {
for {
stream, err := frpConn.AcceptStream(context.Background())
if err != nil {
log.Debugf("Accept new quic mux stream error: %v", err)
log.Debugf("accept new quic mux stream error: %v", err)
_ = frpConn.CloseWithError(0, "")
return
}
@@ -620,7 +620,7 @@ func (svr *Service) RegisterWorkConn(workConn net.Conn, newMsg *msg.NewWorkConn)
xl := netpkg.NewLogFromConn(workConn)
ctl, exist := svr.ctlManager.GetByID(newMsg.RunID)
if !exist {
xl.Warnf("No client control found for run id [%s]", newMsg.RunID)
xl.Warnf("no client control found for run id [%s]", newMsg.RunID)
return fmt.Errorf("no client control found for run id [%s]", newMsg.RunID)
}
// server plugin hook

View File

@@ -38,7 +38,7 @@ func RunE2ETests(t *testing.T) {
// Randomize specs as well as suites
suiteConfig.RandomizeAllSpecs = true
log.Infof("Starting e2e run %q on Ginkgo node %d of total %d",
log.Infof("starting e2e run %q on Ginkgo node %d of total %d",
framework.RunID, suiteConfig.ParallelProcess, suiteConfig.ParallelTotal)
ginkgo.RunSpecs(t, "frp e2e suite", suiteConfig, reporterConfig)
}

View File

@@ -20,7 +20,7 @@ func ExpectResponseCode(code int) EnsureFunc {
if resp.Code == code {
return true
}
flog.Warnf("Expect code %d, but got %d", code, resp.Code)
flog.Warnf("expect code %d, but got %d", code, resp.Code)
return false
}
}
@@ -111,14 +111,14 @@ func (e *RequestExpect) Ensure(fns ...EnsureFunc) {
if len(fns) == 0 {
if !bytes.Equal(e.expectResp, ret.Content) {
flog.Tracef("Response info: %+v", ret)
flog.Tracef("response info: %+v", ret)
}
ExpectEqualValuesWithOffset(1, string(ret.Content), string(e.expectResp), e.explain...)
} else {
for _, fn := range fns {
ok := fn(ret)
if !ok {
flog.Tracef("Response info: %+v", ret)
flog.Tracef("response info: %+v", ret)
}
ExpectTrueWithOffset(1, ok, e.explain...)
}

View File

@@ -93,7 +93,7 @@ var _ = ginkgo.Describe("[Feature: Real IP]", func() {
f.RunProcesses([]string{serverConf}, []string{clientConf})
framework.NewRequestExpect(f).Port(remotePort).Ensure(func(resp *request.Response) bool {
log.Tracef("ProxyProtocol get SourceAddr: %s", string(resp.Content))
log.Tracef("proxy protocol get SourceAddr: %s", string(resp.Content))
addr, err := net.ResolveTCPAddr("tcp", string(resp.Content))
if err != nil {
return false
@@ -142,7 +142,7 @@ var _ = ginkgo.Describe("[Feature: Real IP]", func() {
r.HTTP().HTTPHost("normal.example.com")
}).Ensure(framework.ExpectResponseCode(404))
log.Tracef("ProxyProtocol get SourceAddr: %s", srcAddrRecord)
log.Tracef("proxy protocol get SourceAddr: %s", srcAddrRecord)
addr, err := net.ResolveTCPAddr("tcp", srcAddrRecord)
framework.ExpectNoError(err, srcAddrRecord)
framework.ExpectEqualValues("127.0.0.1", addr.IP.String())

View File

@@ -215,7 +215,7 @@ var _ = ginkgo.Describe("[Feature: Real IP]", func() {
f.RunProcesses([]string{serverConf}, []string{clientConf})
framework.NewRequestExpect(f).Port(remotePort).Ensure(func(resp *request.Response) bool {
log.Tracef("ProxyProtocol get SourceAddr: %s", string(resp.Content))
log.Tracef("proxy protocol get SourceAddr: %s", string(resp.Content))
addr, err := net.ResolveTCPAddr("tcp", string(resp.Content))
if err != nil {
return false
@@ -227,6 +227,56 @@ var _ = ginkgo.Describe("[Feature: Real IP]", func() {
})
})
ginkgo.It("UDP", func() {
serverConf := consts.DefaultServerConfig
clientConf := consts.DefaultClientConfig
localPort := f.AllocPort()
localServer := streamserver.New(streamserver.UDP, streamserver.WithBindPort(localPort),
streamserver.WithCustomHandler(func(c net.Conn) {
defer c.Close()
rd := bufio.NewReader(c)
ppHeader, err := pp.Read(rd)
if err != nil {
log.Errorf("read proxy protocol error: %v", err)
return
}
// Read the actual UDP content after proxy protocol header
if _, err := rpc.ReadBytes(rd); err != nil {
return
}
buf := []byte(ppHeader.SourceAddr.String())
_, _ = rpc.WriteBytes(c, buf)
}))
f.RunServer("", localServer)
remotePort := f.AllocPort()
clientConf += fmt.Sprintf(`
[[proxies]]
name = "udp"
type = "udp"
localPort = %d
remotePort = %d
transport.proxyProtocolVersion = "v2"
`, localPort, remotePort)
f.RunProcesses([]string{serverConf}, []string{clientConf})
framework.NewRequestExpect(f).Protocol("udp").Port(remotePort).Ensure(func(resp *request.Response) bool {
log.Tracef("udp proxy protocol get SourceAddr: %s", string(resp.Content))
addr, err := net.ResolveUDPAddr("udp", string(resp.Content))
if err != nil {
return false
}
if addr.IP.String() != "127.0.0.1" {
return false
}
return true
})
})
ginkgo.It("HTTP", func() {
vhostHTTPPort := f.AllocPort()
serverConf := consts.DefaultServerConfig + fmt.Sprintf(`
@@ -265,7 +315,7 @@ var _ = ginkgo.Describe("[Feature: Real IP]", func() {
r.HTTP().HTTPHost("normal.example.com")
}).Ensure(framework.ExpectResponseCode(404))
log.Tracef("ProxyProtocol get SourceAddr: %s", srcAddrRecord)
log.Tracef("proxy protocol get SourceAddr: %s", srcAddrRecord)
addr, err := net.ResolveTCPAddr("tcp", srcAddrRecord)
framework.ExpectNoError(err, srcAddrRecord)
framework.ExpectEqualValues("127.0.0.1", addr.IP.String())