Compare commits

...

5 Commits

Author SHA1 Message Date
fatedier
c21768ccaa add proxy protocol support for UDP proxies 2025-05-23 21:34:11 +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
15 changed files with 455 additions and 37 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:

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

@ -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

@ -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

@ -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

@ -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

@ -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(`