From 590ccda677afef39763e225fb777c3b2bf0ef4c7 Mon Sep 17 00:00:00 2001 From: fatedier Date: Thu, 28 Mar 2024 16:47:27 +0800 Subject: [PATCH] fix x-forwarded-for header (#4111) --- Release.md | 6 ++ client/proxy/proxy.go | 54 ++++++----- pkg/plugin/client/http2https.go | 3 + pkg/plugin/client/https2http.go | 7 +- pkg/plugin/client/https2https.go | 7 +- pkg/plugin/client/plugin.go | 2 + pkg/util/net/conn.go | 11 ++- pkg/util/vhost/http.go | 1 + test/e2e/framework/request.go | 2 +- test/e2e/v1/features/real_ip.go | 160 +++++++++++++++++++++++++++---- 10 files changed, 203 insertions(+), 50 deletions(-) diff --git a/Release.md b/Release.md index 8b137891..163ca372 100644 --- a/Release.md +++ b/Release.md @@ -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 f806c8f9..16295e05 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 7cd3cd42..65bc2140 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 389ac849..1dc38401 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 da3302b8..dd3245f8 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 520e3794..0e5548e9 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 a5bbe737..20ac73ed 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 34a1400a..7afc7ebb 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" diff --git a/test/e2e/framework/request.go b/test/e2e/framework/request.go index 53d44a3e..1fc67fe8 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 11febaa2..94508f7d 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) + 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(` + 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() {