Repository: fatedier/frp (language: go)

Issue:
https2http gives incorrect X-Forwarded-For
### Bug Description

When using the https2http plugin, the server will send his own ip in the X-Forwarded-For
example:
frp server is 1.2.3.4
frp client is 5.6.7.8
the user is 9.9.9.9
if the user 9.9.9.9 connects to 1.2.3.4, the frp client will recieve `X-Forwarded-For: 1.2.3.4`
the frp client should recieve `X-Forwarded-For: 9.9.9.9`

### frpc Version

0.51.0

### frps Version

0.51.0

### System Architecture

linux/amd64

### Configurations

```
[https2http]
type = https
custom_domains = example.com
plugin = https2http
plugin_local_addr = 127.0.0.1:80
plugin_crt_path = /path/to/fullchain.pem
plugin_key_path = /path/to/privkey.pem
plugin_header_X-From-Where = frp
```

### Logs

_No response_

### Steps to reproduce

_No response_

### Affected area

- [ ] Docs
- [ ] Installation
- [ ] Performance and Scalability
- [ ] Security
- [ ] User Experience
- [ ] Test and Release
- [ ] Developer Infrastructure
- [X] Client Plugin
- [X] Server Plugin
- [ ] Extensions
- [ ] Others


Discussion / hints:
https://github.com/fatedier/frp#http-x-forwarded-for

It is currently only valid for HTTP.
However, perhaps we can achieve it by enabling proxy protocol support in the https2http plugin.
https使用X-Forwarded-For也不支持对么， 以后能支持吗？
will this be supported in the future?

Fix patch:
diff --git a/Release.md b/Release.md
index 8b137891791..163ca3728a1 100644
--- a/Release.md
+++ b/Release.md
@@ -1,1 +1,7 @@
+### Features
 
+* `https2http` and `https2https` plugin now supports `X-Forwared-For` header.
+
+### Fixes
+
+* `X-Forwared-For` header is now correctly set in the request to the backend server for proxy type http.
diff --git a/client/proxy/proxy.go b/client/proxy/proxy.go
index f806c8f9d7f..16295e052c7 100644
--- a/client/proxy/proxy.go
+++ b/client/proxy/proxy.go
@@ -158,33 +158,35 @@ func (pxy *BaseProxy) HandleTCPWorkConnection(workConn net.Conn, m *msg.StartWor
 
 	// check if we need to send proxy protocol info
 	var extraInfo plugin.ExtraInfo
-	if baseCfg.Transport.ProxyProtocolVersion != "" {
-		if m.SrcAddr != "" && m.SrcPort != 0 {
-			if m.DstAddr == "" {
-				m.DstAddr = "127.0.0.1"
-			}
-			srcAddr, _ := net.ResolveTCPAddr("tcp", net.JoinHostPort(m.SrcAddr, strconv.Itoa(int(m.SrcPort))))
-			dstAddr, _ := net.ResolveTCPAddr("tcp", net.JoinHostPort(m.DstAddr, strconv.Itoa(int(m.DstPort))))
-			h := &pp.Header{
-				Command:         pp.PROXY,
-				SourceAddr:      srcAddr,
-				DestinationAddr: 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
-			}
-
-			extraInfo.ProxyProtocolHeader = h
+	if m.SrcAddr != "" && m.SrcPort != 0 {
+		if m.DstAddr == "" {
+			m.DstAddr = "127.0.0.1"
 		}
+		srcAddr, _ := net.ResolveTCPAddr("tcp", net.JoinHostPort(m.SrcAddr, strconv.Itoa(int(m.SrcPort))))
+		dstAddr, _ := net.ResolveTCPAddr("tcp", net.JoinHostPort(m.DstAddr, strconv.Itoa(int(m.DstPort))))
+		extraInfo.SrcAddr = srcAddr
+		extraInfo.DstAddr = dstAddr
+	}
+
+	if baseCfg.Transport.ProxyProtocolVersion != "" && m.SrcAddr != "" && m.SrcPort != 0 {
+		h := &pp.Header{
+			Command:         pp.PROXY,
+			SourceAddr:      extraInfo.SrcAddr,
+			DestinationAddr: extraInfo.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
+		}
+		extraInfo.ProxyProtocolHeader = h
 	}
 
 	if pxy.proxyPlugin != nil {
diff --git a/pkg/plugin/client/http2https.go b/pkg/plugin/client/http2https.go
index 7cd3cd4295f..65bc2140b64 100644
--- a/pkg/plugin/client/http2https.go
+++ b/pkg/plugin/client/http2https.go
@@ -54,6 +54,9 @@ func NewHTTP2HTTPSPlugin(options v1.ClientPluginOptions) (Plugin, error) {
 
 	rp := &httputil.ReverseProxy{
 		Rewrite: func(r *httputil.ProxyRequest) {
+			r.Out.Header["X-Forwarded-For"] = r.In.Header["X-Forwarded-For"]
+			r.Out.Header["X-Forwarded-Host"] = r.In.Header["X-Forwarded-Host"]
+			r.Out.Header["X-Forwarded-Proto"] = r.In.Header["X-Forwarded-Proto"]
 			req := r.Out
 			req.URL.Scheme = "https"
 			req.URL.Host = p.opts.LocalAddr
diff --git a/pkg/plugin/client/https2http.go b/pkg/plugin/client/https2http.go
index 389ac849eb8..1dc3840180d 100644
--- a/pkg/plugin/client/https2http.go
+++ b/pkg/plugin/client/https2http.go
@@ -51,6 +51,8 @@ func NewHTTPS2HTTPPlugin(options v1.ClientPluginOptions) (Plugin, error) {
 
 	rp := &httputil.ReverseProxy{
 		Rewrite: func(r *httputil.ProxyRequest) {
+			r.Out.Header["X-Forwarded-For"] = r.In.Header["X-Forwarded-For"]
+			r.SetXForwarded()
 			req := r.Out
 			req.URL.Scheme = "http"
 			req.URL.Host = p.opts.LocalAddr
@@ -98,8 +100,11 @@ func (p *HTTPS2HTTPPlugin) genTLSConfig() (*tls.Config, error) {
 	return config, nil
 }
 
-func (p *HTTPS2HTTPPlugin) Handle(conn io.ReadWriteCloser, realConn net.Conn, _ *ExtraInfo) {
+func (p *HTTPS2HTTPPlugin) Handle(conn io.ReadWriteCloser, realConn net.Conn, extra *ExtraInfo) {
 	wrapConn := netpkg.WrapReadWriteCloserToConn(conn, realConn)
+	if extra.SrcAddr != nil {
+		wrapConn.SetRemoteAddr(extra.SrcAddr)
+	}
 	_ = p.l.PutConn(wrapConn)
 }
 
diff --git a/pkg/plugin/client/https2https.go b/pkg/plugin/client/https2https.go
index da3302b80d2..dd3245f83be 100644
--- a/pkg/plugin/client/https2https.go
+++ b/pkg/plugin/client/https2https.go
@@ -56,6 +56,8 @@ func NewHTTPS2HTTPSPlugin(options v1.ClientPluginOptions) (Plugin, error) {
 
 	rp := &httputil.ReverseProxy{
 		Rewrite: func(r *httputil.ProxyRequest) {
+			r.Out.Header["X-Forwarded-For"] = r.In.Header["X-Forwarded-For"]
+			r.SetXForwarded()
 			req := r.Out
 			req.URL.Scheme = "https"
 			req.URL.Host = p.opts.LocalAddr
@@ -104,8 +106,11 @@ func (p *HTTPS2HTTPSPlugin) genTLSConfig() (*tls.Config, error) {
 	return config, nil
 }
 
-func (p *HTTPS2HTTPSPlugin) Handle(conn io.ReadWriteCloser, realConn net.Conn, _ *ExtraInfo) {
+func (p *HTTPS2HTTPSPlugin) Handle(conn io.ReadWriteCloser, realConn net.Conn, extra *ExtraInfo) {
 	wrapConn := netpkg.WrapReadWriteCloserToConn(conn, realConn)
+	if extra.SrcAddr != nil {
+		wrapConn.SetRemoteAddr(extra.SrcAddr)
+	}
 	_ = p.l.PutConn(wrapConn)
 }
 
diff --git a/pkg/plugin/client/plugin.go b/pkg/plugin/client/plugin.go
index 520e379441f..0e5548e9f4e 100644
--- a/pkg/plugin/client/plugin.go
+++ b/pkg/plugin/client/plugin.go
@@ -50,6 +50,8 @@ func Create(name string, options v1.ClientPluginOptions) (p Plugin, err error) {
 
 type ExtraInfo struct {
 	ProxyProtocolHeader *pp.Header
+	SrcAddr             net.Addr
+	DstAddr             net.Addr
 }
 
 type Plugin interface {
diff --git a/pkg/util/net/conn.go b/pkg/util/net/conn.go
index a5bbe7371c5..20ac73ed1c6 100644
--- a/pkg/util/net/conn.go
+++ b/pkg/util/net/conn.go
@@ -76,9 +76,11 @@ type WrapReadWriteCloserConn struct {
 	io.ReadWriteCloser
 
 	underConn net.Conn
+
+	remoteAddr net.Addr
 }
 
-func WrapReadWriteCloserToConn(rwc io.ReadWriteCloser, underConn net.Conn) net.Conn {
+func WrapReadWriteCloserToConn(rwc io.ReadWriteCloser, underConn net.Conn) *WrapReadWriteCloserConn {
 	return &WrapReadWriteCloserConn{
 		ReadWriteCloser: rwc,
 		underConn:       underConn,
@@ -92,7 +94,14 @@ func (conn *WrapReadWriteCloserConn) LocalAddr() net.Addr {
 	return (*net.TCPAddr)(nil)
 }
 
+func (conn *WrapReadWriteCloserConn) SetRemoteAddr(addr net.Addr) {
+	conn.remoteAddr = addr
+}
+
 func (conn *WrapReadWriteCloserConn) RemoteAddr() net.Addr {
+	if conn.remoteAddr != nil {
+		return conn.remoteAddr
+	}
 	if conn.underConn != nil {
 		return conn.underConn.RemoteAddr()
 	}
diff --git a/pkg/util/vhost/http.go b/pkg/util/vhost/http.go
index 34a1400ac6f..7afc7ebbf19 100644
--- a/pkg/util/vhost/http.go
+++ b/pkg/util/vhost/http.go
@@ -59,6 +59,7 @@ func NewHTTPReverseProxy(option HTTPReverseProxyOptions, vhostRouter *Routers) *
 	proxy := &httputil.ReverseProxy{
 		// Modify incoming requests by route policies.
 		Rewrite: func(r *httputil.ProxyRequest) {
+			r.Out.Header["X-Forwarded-For"] = r.In.Header["X-Forwarded-For"]
 			r.SetXForwarded()
 			req := r.Out
 			req.URL.Scheme = "http"


Test patch:
diff --git a/test/e2e/framework/request.go b/test/e2e/framework/request.go
index 53d44a3eac4..1fc67fe86ab 100644
--- a/test/e2e/framework/request.go
+++ b/test/e2e/framework/request.go
@@ -113,7 +113,7 @@ func (e *RequestExpect) Ensure(fns ...EnsureFunc) {
 		if !bytes.Equal(e.expectResp, ret.Content) {
 			flog.Tracef("Response info: %+v", ret)
 		}
-		ExpectEqualValuesWithOffset(1, ret.Content, e.expectResp, e.explain...)
+		ExpectEqualValuesWithOffset(1, string(ret.Content), string(e.expectResp), e.explain...)
 	} else {
 		for _, fn := range fns {
 			ok := fn(ret)
diff --git a/test/e2e/v1/features/real_ip.go b/test/e2e/v1/features/real_ip.go
index 11febaa245d..94508f7da9a 100644
--- a/test/e2e/v1/features/real_ip.go
+++ b/test/e2e/v1/features/real_ip.go
@@ -2,6 +2,7 @@ package features
 
 import (
 	"bufio"
+	"crypto/tls"
 	"fmt"
 	"net"
 	"net/http"
@@ -9,6 +10,7 @@ import (
 	"github.com/onsi/ginkgo/v2"
 	pp "github.com/pires/go-proxyproto"
 
+	"github.com/fatedier/frp/pkg/transport"
 	"github.com/fatedier/frp/pkg/util/log"
 	"github.com/fatedier/frp/test/e2e/framework"
 	"github.com/fatedier/frp/test/e2e/framework/consts"
@@ -21,23 +23,24 @@ import (
 var _ = ginkgo.Describe("[Feature: Real IP]", func() {
 	f := framework.NewDefaultFramework()
 
-	ginkgo.It("HTTP X-Forwarded-For", func() {
-		vhostHTTPPort := f.AllocPort()
-		serverConf := consts.DefaultServerConfig + fmt.Sprintf(`
+	ginkgo.Describe("HTTP X-forwarded-For", func() {
+		ginkgo.It("Client Without Header", func() {
+			vhostHTTPPort := f.AllocPort()
+			serverConf := consts.DefaultServerConfig + fmt.Sprintf(`
 		vhostHTTPPort = %d
 		`, vhostHTTPPort)
 
-		localPort := f.AllocPort()
-		localServer := httpserver.New(
-			httpserver.WithBindPort(localPort),
-			httpserver.WithHandler(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
-				_, _ = w.Write([]byte(req.Header.Get("X-Forwarded-For")))
-			})),
-		)
-		f.RunServer("", localServer)
-
-		clientConf := consts.DefaultClientConfig
-		clientConf += fmt.Sprintf(`
+			localPort := f.AllocPort()
+			localServer := httpserver.New(
+				httpserver.WithBindPort(localPort),
+				httpserver.WithHandler(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+					_, _ = w.Write([]byte(req.Header.Get("X-Forwarded-For")))
+				})),
+			)
+			f.RunServer("", localServer)
+
+			clientConf := consts.DefaultClientConfig
+			clientConf += fmt.Sprintf(`
 		[[proxies]]
 		name = "test"
 		type = "http"
@@ -45,14 +48,131 @@ var _ = ginkgo.Describe("[Feature: Real IP]", func() {
 		customDomains = ["normal.example.com"]
 		`, localPort)
 
-		f.RunProcesses([]string{serverConf}, []string{clientConf})
+			f.RunProcesses([]string{serverConf}, []string{clientConf})
 
-		framework.NewRequestExpect(f).Port(vhostHTTPPort).
-			RequestModify(func(r *request.Request) {
-				r.HTTP().HTTPHost("normal.example.com")
-			}).
-			ExpectResp([]byte("127.0.0.1")).
-			Ensure()
+			framework.NewRequestExpect(f).Port(vhostHTTPPort).
+				RequestModify(func(r *request.Request) {
+					r.HTTP().HTTPHost("normal.example.com")
+				}).
+				ExpectResp([]byte("127.0.0.1")).
+				Ensure()
+		})
+
+		ginkgo.It("Client With Header", func() {
+			vhostHTTPPort := f.AllocPort()
+			serverConf := consts.DefaultServerConfig + fmt.Sprintf(`
+		vhostHTTPPort = %d
+		`, vhostHTTPPort)
+
+			localPort := f.AllocPort()
+			localServer := httpserver.New(
+				httpserver.WithBindPort(localPort),
+				httpserver.WithHandler(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+					_, _ = w.Write([]byte(req.Header.Get("X-Forwarded-For")))
+				})),
+			)
+			f.RunServer("", localServer)
+
+			clientConf := consts.DefaultClientConfig
+			clientConf += fmt.Sprintf(`
+		[[proxies]]
+		name = "test"
+		type = "http"
+		localPort = %d
+		customDomains = ["normal.example.com"]
+		`, localPort)
+
+			f.RunProcesses([]string{serverConf}, []string{clientConf})
+
+			framework.NewRequestExpect(f).Port(vhostHTTPPort).
+				RequestModify(func(r *request.Request) {
+					r.HTTP().HTTPHost("normal.example.com")
+					r.HTTP().HTTPHeaders(map[string]string{"x-forwarded-for": "2.2.2.2"})
+				}).
+				ExpectResp([]byte("2.2.2.2, 127.0.0.1")).
+				Ensure()
+		})
+
+		ginkgo.It("http2https plugin", func() {
+			vhostHTTPPort := f.AllocPort()
+			serverConf := consts.DefaultServerConfig + fmt.Sprintf(`
+		vhostHTTPPort = %d
+		`, vhostHTTPPort)
+
+			localPort := f.AllocPort()
+
+			clientConf := consts.DefaultClientConfig
+			clientConf += fmt.Sprintf(`
+		[[proxies]]
+		name = "test"
+		type = "http"
+		customDomains = ["normal.example.com"]
+		[proxies.plugin]
+		type = "http2https"
+		localAddr = "127.0.0.1:%d"
+		`, localPort)
+
+			f.RunProcesses([]string{serverConf}, []string{clientConf})
+
+			tlsConfig, err := transport.NewServerTLSConfig("", "", "")
+			framework.ExpectNoError(err)
+
+			localServer := httpserver.New(
+				httpserver.WithBindPort(localPort),
+				httpserver.WithHandler(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+					_, _ = w.Write([]byte(req.Header.Get("X-Forwarded-For")))
+				})),
+				httpserver.WithTLSConfig(tlsConfig),
+			)
+			f.RunServer("", localServer)
+
+			framework.NewRequestExpect(f).Port(vhostHTTPPort).
+				RequestModify(func(r *request.Request) {
+					r.HTTP().HTTPHost("normal.example.com")
+					r.HTTP().HTTPHeaders(map[string]string{"x-forwarded-for": "2.2.2.2, 3.3.3.3"})
+				}).
+				ExpectResp([]byte("2.2.2.2, 3.3.3.3, 127.0.0.1")).
+				Ensure()
+		})
+
+		ginkgo.It("https2http plugin", func() {
+			vhostHTTPSPort := f.AllocPort()
+			serverConf := consts.DefaultServerConfig + fmt.Sprintf(`
+		vhostHTTPSPort = %d
+		`, vhostHTTPSPort)
+
+			localPort := f.AllocPort()
+
+			clientConf := consts.DefaultClientConfig
+			clientConf += fmt.Sprintf(`
+		[[proxies]]
+		name = "test"
+		type = "https"
+		customDomains = ["normal.example.com"]
+		[proxies.plugin]
+		type = "https2http"
+		localAddr = "127.0.0.1:%d"
+		`, localPort)
+
+			f.RunProcesses([]string{serverConf}, []string{clientConf})
+
+			localServer := httpserver.New(
+				httpserver.WithBindPort(localPort),
+				httpserver.WithHandler(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+					_, _ = w.Write([]byte(req.Header.Get("X-Forwarded-For")))
+				})),
+			)
+			f.RunServer("", localServer)
+
+			framework.NewRequestExpect(f).Port(vhostHTTPSPort).
+				RequestModify(func(r *request.Request) {
+					r.HTTPS().HTTPHost("normal.example.com").
+						HTTPHeaders(map[string]string{"x-forwarded-for": "2.2.2.2"}).
+						TLSConfig(&tls.Config{ServerName: "normal.example.com", InsecureSkipVerify: true})
+				}).
+				ExpectResp([]byte("2.2.2.2, 127.0.0.1")).
+				Ensure()
+		})
 	})
 
 	ginkgo.Describe("Proxy Protocol", func() {