Skip to content

Commit 3fa76b7

Browse files
committed
add proxy protocol support for UDP proxies (#4810)
1 parent 8eb525a commit 3fa76b7

File tree

9 files changed

+299
-25
lines changed

9 files changed

+299
-25
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1025,7 +1025,7 @@ You can get user's real IP from HTTP request headers `X-Forwarded-For`.
10251025

10261026
#### Proxy Protocol
10271027

1028-
frp supports Proxy Protocol to send user's real IP to local services. It support all types except UDP.
1028+
frp supports Proxy Protocol to send user's real IP to local services.
10291029

10301030
Here is an example for https service:
10311031

Release.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
## Features
22

3-
* Support for YAML merge functionality (anchors and references with dot-prefixed fields) in strict configuration mode without requiring `--strict-config=false` parameter.
3+
* Support for YAML merge functionality (anchors and references with dot-prefixed fields) in strict configuration mode without requiring `--strict-config=false` parameter.
4+
* Support for proxy protocol in UDP proxies to preserve real client IP addresses.

client/proxy/proxy.go

Lines changed: 4 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,11 @@ import (
2020
"net"
2121
"reflect"
2222
"strconv"
23-
"strings"
2423
"sync"
2524
"time"
2625

2726
libio "github.com/fatedier/golib/io"
2827
libnet "github.com/fatedier/golib/net"
29-
pp "github.com/pires/go-proxyproto"
3028
"golang.org/x/time/rate"
3129

3230
"github.com/fatedier/frp/pkg/config/types"
@@ -35,6 +33,7 @@ import (
3533
plugin "github.com/fatedier/frp/pkg/plugin/client"
3634
"github.com/fatedier/frp/pkg/transport"
3735
"github.com/fatedier/frp/pkg/util/limit"
36+
netpkg "github.com/fatedier/frp/pkg/util/net"
3837
"github.com/fatedier/frp/pkg/util/xlog"
3938
"github.com/fatedier/frp/pkg/vnet"
4039
)
@@ -176,24 +175,9 @@ func (pxy *BaseProxy) HandleTCPWorkConnection(workConn net.Conn, m *msg.StartWor
176175
}
177176

178177
if baseCfg.Transport.ProxyProtocolVersion != "" && m.SrcAddr != "" && m.SrcPort != 0 {
179-
h := &pp.Header{
180-
Command: pp.PROXY,
181-
SourceAddr: connInfo.SrcAddr,
182-
DestinationAddr: connInfo.DstAddr,
183-
}
184-
185-
if strings.Contains(m.SrcAddr, ".") {
186-
h.TransportProtocol = pp.TCPv4
187-
} else {
188-
h.TransportProtocol = pp.TCPv6
189-
}
190-
191-
if baseCfg.Transport.ProxyProtocolVersion == "v1" {
192-
h.Version = 1
193-
} else if baseCfg.Transport.ProxyProtocolVersion == "v2" {
194-
h.Version = 2
195-
}
196-
connInfo.ProxyProtocolHeader = h
178+
// Use the common proxy protocol builder function
179+
header := netpkg.BuildProxyProtocolHeaderStruct(connInfo.SrcAddr, connInfo.DstAddr, baseCfg.Transport.ProxyProtocolVersion)
180+
connInfo.ProxyProtocolHeader = header
197181
}
198182
connInfo.Conn = remote
199183
connInfo.UnderlyingConn = workConn

client/proxy/sudp.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,5 +205,5 @@ func (pxy *SUDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) {
205205
go workConnReaderFn(workConn, readCh)
206206
go heartbeatFn(sendCh)
207207

208-
udp.Forwarder(pxy.localAddr, readCh, sendCh, int(pxy.clientCfg.UDPPacketSize))
208+
udp.Forwarder(pxy.localAddr, readCh, sendCh, int(pxy.clientCfg.UDPPacketSize), pxy.cfg.Transport.ProxyProtocolVersion)
209209
}

client/proxy/udp.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,5 +171,7 @@ func (pxy *UDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) {
171171
go workConnSenderFn(pxy.workConn, pxy.sendCh)
172172
go workConnReaderFn(pxy.workConn, pxy.readCh)
173173
go heartbeatFn(pxy.sendCh)
174-
udp.Forwarder(pxy.localAddr, pxy.readCh, pxy.sendCh, int(pxy.clientCfg.UDPPacketSize))
174+
175+
// Call Forwarder with proxy protocol version (empty string means no proxy protocol)
176+
udp.Forwarder(pxy.localAddr, pxy.readCh, pxy.sendCh, int(pxy.clientCfg.UDPPacketSize), pxy.cfg.Transport.ProxyProtocolVersion)
175177
}

pkg/proto/udp/udp.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"github.com/fatedier/golib/pool"
2525

2626
"github.com/fatedier/frp/pkg/msg"
27+
netpkg "github.com/fatedier/frp/pkg/util/net"
2728
)
2829

2930
func NewUDPPacket(buf []byte, laddr, raddr *net.UDPAddr) *msg.UDPPacket {
@@ -69,7 +70,7 @@ func ForwardUserConn(udpConn *net.UDPConn, readCh <-chan *msg.UDPPacket, sendCh
6970
}
7071
}
7172

72-
func Forwarder(dstAddr *net.UDPAddr, readCh <-chan *msg.UDPPacket, sendCh chan<- msg.Message, bufSize int) {
73+
func Forwarder(dstAddr *net.UDPAddr, readCh <-chan *msg.UDPPacket, sendCh chan<- msg.Message, bufSize int, proxyProtocolVersion string) {
7374
var mu sync.RWMutex
7475
udpConnMap := make(map[string]*net.UDPConn)
7576

@@ -110,6 +111,7 @@ func Forwarder(dstAddr *net.UDPAddr, readCh <-chan *msg.UDPPacket, sendCh chan<-
110111
if err != nil {
111112
continue
112113
}
114+
113115
mu.Lock()
114116
udpConn, ok := udpConnMap[udpMsg.RemoteAddr.String()]
115117
if !ok {
@@ -122,6 +124,18 @@ func Forwarder(dstAddr *net.UDPAddr, readCh <-chan *msg.UDPPacket, sendCh chan<-
122124
}
123125
mu.Unlock()
124126

127+
// Add proxy protocol header if configured
128+
if proxyProtocolVersion != "" && udpMsg.RemoteAddr != nil {
129+
ppBuf, err := netpkg.BuildProxyProtocolHeader(udpMsg.RemoteAddr, dstAddr, proxyProtocolVersion)
130+
if err == nil {
131+
// Prepend proxy protocol header to the UDP payload
132+
finalBuf := make([]byte, len(ppBuf)+len(buf))
133+
copy(finalBuf, ppBuf)
134+
copy(finalBuf[len(ppBuf):], buf)
135+
buf = finalBuf
136+
}
137+
}
138+
125139
_, err = udpConn.Write(buf)
126140
if err != nil {
127141
udpConn.Close()

pkg/util/net/proxyprotocol.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Copyright 2025 The frp Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package net
16+
17+
import (
18+
"bytes"
19+
"fmt"
20+
"net"
21+
22+
pp "github.com/pires/go-proxyproto"
23+
)
24+
25+
func BuildProxyProtocolHeaderStruct(srcAddr, dstAddr net.Addr, version string) *pp.Header {
26+
var versionByte byte
27+
if version == "v1" {
28+
versionByte = 1
29+
} else {
30+
versionByte = 2 // default to v2
31+
}
32+
return pp.HeaderProxyFromAddrs(versionByte, srcAddr, dstAddr)
33+
}
34+
35+
func BuildProxyProtocolHeader(srcAddr, dstAddr net.Addr, version string) ([]byte, error) {
36+
h := BuildProxyProtocolHeaderStruct(srcAddr, dstAddr, version)
37+
38+
// Convert header to bytes using a buffer
39+
var buf bytes.Buffer
40+
_, err := h.WriteTo(&buf)
41+
if err != nil {
42+
return nil, fmt.Errorf("failed to write proxy protocol header: %v", err)
43+
}
44+
return buf.Bytes(), nil
45+
}

pkg/util/net/proxyprotocol_test.go

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
package net
2+
3+
import (
4+
"net"
5+
"testing"
6+
7+
pp "github.com/pires/go-proxyproto"
8+
"github.com/stretchr/testify/require"
9+
)
10+
11+
func TestBuildProxyProtocolHeader(t *testing.T) {
12+
require := require.New(t)
13+
14+
tests := []struct {
15+
name string
16+
srcAddr net.Addr
17+
dstAddr net.Addr
18+
version string
19+
expectError bool
20+
}{
21+
{
22+
name: "UDP IPv4 v2",
23+
srcAddr: &net.UDPAddr{IP: net.ParseIP("192.168.1.100"), Port: 12345},
24+
dstAddr: &net.UDPAddr{IP: net.ParseIP("10.0.0.1"), Port: 3306},
25+
version: "v2",
26+
expectError: false,
27+
},
28+
{
29+
name: "TCP IPv4 v1",
30+
srcAddr: &net.TCPAddr{IP: net.ParseIP("192.168.1.100"), Port: 12345},
31+
dstAddr: &net.TCPAddr{IP: net.ParseIP("10.0.0.1"), Port: 80},
32+
version: "v1",
33+
expectError: false,
34+
},
35+
{
36+
name: "UDP IPv6 v2",
37+
srcAddr: &net.UDPAddr{IP: net.ParseIP("2001:db8::1"), Port: 12345},
38+
dstAddr: &net.UDPAddr{IP: net.ParseIP("::1"), Port: 3306},
39+
version: "v2",
40+
expectError: false,
41+
},
42+
{
43+
name: "TCP IPv6 v1",
44+
srcAddr: &net.TCPAddr{IP: net.ParseIP("::1"), Port: 12345},
45+
dstAddr: &net.TCPAddr{IP: net.ParseIP("2001:db8::1"), Port: 80},
46+
version: "v1",
47+
expectError: false,
48+
},
49+
{
50+
name: "nil source address",
51+
srcAddr: nil,
52+
dstAddr: &net.UDPAddr{IP: net.ParseIP("10.0.0.1"), Port: 3306},
53+
version: "v2",
54+
expectError: false,
55+
},
56+
{
57+
name: "nil destination address",
58+
srcAddr: &net.TCPAddr{IP: net.ParseIP("192.168.1.100"), Port: 12345},
59+
dstAddr: nil,
60+
version: "v2",
61+
expectError: false,
62+
},
63+
{
64+
name: "unsupported address type",
65+
srcAddr: &net.UnixAddr{Name: "/tmp/test.sock", Net: "unix"},
66+
dstAddr: &net.UDPAddr{IP: net.ParseIP("10.0.0.1"), Port: 3306},
67+
version: "v2",
68+
expectError: false,
69+
},
70+
}
71+
72+
for _, tt := range tests {
73+
header, err := BuildProxyProtocolHeader(tt.srcAddr, tt.dstAddr, tt.version)
74+
75+
if tt.expectError {
76+
require.Error(err, "test case: %s", tt.name)
77+
continue
78+
}
79+
80+
require.NoError(err, "test case: %s", tt.name)
81+
require.NotEmpty(header, "test case: %s", tt.name)
82+
}
83+
}
84+
85+
func TestBuildProxyProtocolHeaderStruct(t *testing.T) {
86+
require := require.New(t)
87+
88+
tests := []struct {
89+
name string
90+
srcAddr net.Addr
91+
dstAddr net.Addr
92+
version string
93+
expectedProtocol pp.AddressFamilyAndProtocol
94+
expectedVersion byte
95+
expectedCommand pp.ProtocolVersionAndCommand
96+
expectedSourceAddr net.Addr
97+
expectedDestAddr net.Addr
98+
}{
99+
{
100+
name: "TCP IPv4 v2",
101+
srcAddr: &net.TCPAddr{IP: net.ParseIP("192.168.1.100"), Port: 12345},
102+
dstAddr: &net.TCPAddr{IP: net.ParseIP("10.0.0.1"), Port: 80},
103+
version: "v2",
104+
expectedProtocol: pp.TCPv4,
105+
expectedVersion: 2,
106+
expectedCommand: pp.PROXY,
107+
expectedSourceAddr: &net.TCPAddr{IP: net.ParseIP("192.168.1.100"), Port: 12345},
108+
expectedDestAddr: &net.TCPAddr{IP: net.ParseIP("10.0.0.1"), Port: 80},
109+
},
110+
{
111+
name: "UDP IPv6 v1",
112+
srcAddr: &net.UDPAddr{IP: net.ParseIP("2001:db8::1"), Port: 12345},
113+
dstAddr: &net.UDPAddr{IP: net.ParseIP("::1"), Port: 3306},
114+
version: "v1",
115+
expectedProtocol: pp.UDPv6,
116+
expectedVersion: 1,
117+
expectedCommand: pp.PROXY,
118+
expectedSourceAddr: &net.UDPAddr{IP: net.ParseIP("2001:db8::1"), Port: 12345},
119+
expectedDestAddr: &net.UDPAddr{IP: net.ParseIP("::1"), Port: 3306},
120+
},
121+
{
122+
name: "TCP IPv6 default version",
123+
srcAddr: &net.TCPAddr{IP: net.ParseIP("::1"), Port: 12345},
124+
dstAddr: &net.TCPAddr{IP: net.ParseIP("2001:db8::1"), Port: 80},
125+
version: "",
126+
expectedProtocol: pp.TCPv6,
127+
expectedVersion: 2, // default to v2
128+
expectedCommand: pp.PROXY,
129+
expectedSourceAddr: &net.TCPAddr{IP: net.ParseIP("::1"), Port: 12345},
130+
expectedDestAddr: &net.TCPAddr{IP: net.ParseIP("2001:db8::1"), Port: 80},
131+
},
132+
{
133+
name: "nil source address",
134+
srcAddr: nil,
135+
dstAddr: &net.UDPAddr{IP: net.ParseIP("10.0.0.1"), Port: 3306},
136+
version: "v2",
137+
expectedProtocol: pp.UNSPEC,
138+
expectedVersion: 2,
139+
expectedCommand: pp.LOCAL,
140+
expectedSourceAddr: nil, // go-proxyproto sets both to nil when srcAddr is nil
141+
expectedDestAddr: nil,
142+
},
143+
{
144+
name: "nil destination address",
145+
srcAddr: &net.TCPAddr{IP: net.ParseIP("192.168.1.100"), Port: 12345},
146+
dstAddr: nil,
147+
version: "v2",
148+
expectedProtocol: pp.UNSPEC,
149+
expectedVersion: 2,
150+
expectedCommand: pp.LOCAL,
151+
expectedSourceAddr: nil, // go-proxyproto sets both to nil when dstAddr is nil
152+
expectedDestAddr: nil,
153+
},
154+
{
155+
name: "unsupported address type",
156+
srcAddr: &net.UnixAddr{Name: "/tmp/test.sock", Net: "unix"},
157+
dstAddr: &net.UDPAddr{IP: net.ParseIP("10.0.0.1"), Port: 3306},
158+
version: "v2",
159+
expectedProtocol: pp.UNSPEC,
160+
expectedVersion: 2,
161+
expectedCommand: pp.LOCAL,
162+
expectedSourceAddr: nil, // go-proxyproto sets both to nil for unsupported types
163+
expectedDestAddr: nil,
164+
},
165+
}
166+
167+
for _, tt := range tests {
168+
header := BuildProxyProtocolHeaderStruct(tt.srcAddr, tt.dstAddr, tt.version)
169+
170+
require.NotNil(header, "test case: %s", tt.name)
171+
172+
require.Equal(tt.expectedCommand, header.Command, "test case: %s", tt.name)
173+
require.Equal(tt.expectedSourceAddr, header.SourceAddr, "test case: %s", tt.name)
174+
require.Equal(tt.expectedDestAddr, header.DestinationAddr, "test case: %s", tt.name)
175+
require.Equal(tt.expectedProtocol, header.TransportProtocol, "test case: %s", tt.name)
176+
require.Equal(tt.expectedVersion, header.Version, "test case: %s", tt.name)
177+
}
178+
}

0 commit comments

Comments
 (0)