diff --git a/docs/middlewares.md b/docs/middlewares.md index 39c6b3a..4c98a7e 100644 --- a/docs/middlewares.md +++ b/docs/middlewares.md @@ -9,12 +9,15 @@ - [Available middlewares](#available-middlewares) - [Redirect http](#redirect-http) - [Custom error pages](#custom-error-pages) + - [Real IP](#real-ip) + - [Custom](#custom) + - [Cloudflare](#cloudflare) - [Modify request or response](#modify-request-or-response) - [Set headers](#set-headers) - [Add headers](#add-headers) - [Hide headers](#hide-headers) - [X-Forwarded-\* Headers](#x-forwarded--headers) - - [Add X-Forwarded-\*](#add-x-forwarded-) + - [Hide X-Forwarded-\*](#hide-x-forwarded-) - [Set X-Forwarded-\*](#set-x-forwarded-) - [Forward Authorization header (experimental)](#forward-authorization-header-experimental) - [Examples](#examples) @@ -104,6 +107,67 @@ location / { [🔼Back to top](#table-of-content) +### Real IP + +Check https://nginx.org/en/docs/http/ngx_http_realip_module.html for explaination of options + +#### Custom + +```yaml +# docker labels +proxy.app1.middlewares.real_ip.header: X-Real-IP +proxy.app1.middlewares.real_ip.from: | + - 127.0.0.1 + - 192.168.0.0/16 + - 10.0.0.0/8 +proxy.app1.middlewares.real_ip.recursive: true + +# include file +app1: + middlewares: + real_ip: + header: X-Real-IP + from: + - 127.0.0.1 + - 192.168.0.0/16 + - 10.0.0.0/8 + recursive: true +``` + +nginx equivalent: +```nginx +location / { + set_real_ip_from 127.0.0.1; + set_real_ip_from 192.168.0.0/16; + set_real_ip_from 10.0.0.0/8; + + real_ip_header X-Real-IP; + real_ip_recursive on; +} +``` + +#### Cloudflare + +This is a preset for Cloudflare + +- `header`: `CF-Connecting-IP` +- `from`: CIDR List of Cloudflare IPs from (updated every hour) + - https://www.cloudflare.com/ips-v4 + - https://www.cloudflare.com/ips-v6 +- `recursive`: true + +```yaml +# docker labels +proxy.app1.middlewares.cloudflare_real_ip: + +# include file +app1: + middlewares: + cloudflare_real_ip: +``` + +[🔼Back to top](#table-of-content) + ### Modify request or response ```yaml @@ -199,21 +263,23 @@ location / { } ``` +[🔼Back to top](#table-of-content) + ### X-Forwarded-* Headers -#### Add X-Forwarded-* +#### Hide X-Forwarded-* -Append `X-Forwarded-*` headers to existing headers +Remove `Forwarded` and `X-Forwarded-*` headers before request ```yaml # docker labels -proxy.app1.middlewares.modify_request.add_x_forwarded: +proxy.app1.middlewares.modify_request.hide_x_forwarded: # include file app1: middlewares: modify_request: - add_x_forwarded: + hide_x_forwarded: ``` #### Set X-Forwarded-* diff --git a/internal/net/http/middleware/middlewares.go b/internal/net/http/middleware/middlewares.go index 87e0824..db70d57 100644 --- a/internal/net/http/middleware/middlewares.go +++ b/internal/net/http/middleware/middlewares.go @@ -4,6 +4,7 @@ import ( "fmt" "strings" + "github.com/sirupsen/logrus" D "github.com/yusing/go-proxy/internal/docker" ) @@ -18,7 +19,7 @@ func Get(name string) (middleware *Middleware, ok bool) { func init() { middlewares = map[string]*Middleware{ "set_x_forwarded": SetXForwarded, - "add_x_forwarded": AddXForwarded, + "hide_x_forwarded": HideXForwarded, "redirect_http": RedirectHTTP, "forward_auth": ForwardAuth.m, "modify_response": ModifyResponse.m, @@ -44,4 +45,28 @@ func init() { m.name = names[0] } } + // TODO: seperate from init() + // b := E.NewBuilder("failed to load middlewares") + // middlewareDefs, err := U.ListFiles(common.MiddlewareDefsBasePath, 0) + // if err != nil { + // logrus.Errorf("failed to list middleware definitions: %s", err) + // return + // } + // for _, defFile := range middlewareDefs { + // mws, err := BuildMiddlewaresFromYAML(defFile) + // for name, m := range mws { + // if _, ok := middlewares[name]; ok { + // b.Add(E.Duplicated("middleware", name)) + // continue + // } + // middlewares[name] = m + // logger.Infof("middleware %s loaded from %s", name, path.Base(defFile)) + // } + // b.Add(err.Subject(defFile)) + // } + // if b.HasError() { + // logger.Error(b.Build()) + // } } + +var logger = logrus.WithField("module", "middlewares") diff --git a/internal/net/http/middleware/x_forwarded.go b/internal/net/http/middleware/x_forwarded.go index 624996b..9a2f4d3 100644 --- a/internal/net/http/middleware/x_forwarded.go +++ b/internal/net/http/middleware/x_forwarded.go @@ -2,34 +2,16 @@ package middleware import ( "net" - "strings" ) -var AddXForwarded = &Middleware{ - rewrite: func(req *Request) { - clientIP, _, err := net.SplitHostPort(req.RemoteAddr) - if err == nil { - req.Header.Set("X-Forwarded-For", clientIP) - } else { - req.Header.Del("X-Forwarded-For") - } - req.Header.Set("X-Forwarded-Host", req.Host) - if req.TLS == nil { - req.Header.Set("X-Forwarded-Proto", "http") - } else { - req.Header.Set("X-Forwarded-Proto", "https") - } - }, -} - var SetXForwarded = &Middleware{ rewrite: func(req *Request) { + req.Header.Del("Forwarded") + req.Header.Del("X-Forwarded-For") + req.Header.Del("X-Forwarded-Host") + req.Header.Del("X-Forwarded-Proto") clientIP, _, err := net.SplitHostPort(req.RemoteAddr) if err == nil { - prior := req.Header["X-Forwarded-For"] - if len(prior) > 0 { - clientIP = strings.Join(prior, ", ") + ", " + clientIP - } req.Header.Set("X-Forwarded-For", clientIP) } else { req.Header.Del("X-Forwarded-For") @@ -42,3 +24,12 @@ var SetXForwarded = &Middleware{ } }, } + +var HideXForwarded = &Middleware{ + rewrite: func(req *Request) { + req.Header.Del("Forwarded") + req.Header.Del("X-Forwarded-For") + req.Header.Del("X-Forwarded-Host") + req.Header.Del("X-Forwarded-Proto") + }, +} diff --git a/internal/net/http/reverse_proxy_mod.go b/internal/net/http/reverse_proxy_mod.go index 0570944..d3f5177 100644 --- a/internal/net/http/reverse_proxy_mod.go +++ b/internal/net/http/reverse_proxy_mod.go @@ -15,11 +15,13 @@ import ( "errors" "fmt" "io" + "net" "net/http" "net/http/httptrace" "net/textproto" "net/url" "strings" + "sync" "github.com/sirupsen/logrus" "golang.org/x/net/http/httpguts" @@ -65,28 +67,35 @@ type ProxyRequest struct { // 1xx responses are forwarded to the client if the underlying // transport supports ClientTrace.Got1xxResponse. type ReverseProxy struct { - // Rewrite must be a function which modifies + // Director is a function which modifies // the request into a new request to be sent // using Transport. Its response is then copied // back to the original client unmodified. - // Rewrite must not access the provided ProxyRequest - // or its contents after returning. + // Director must not access the provided Request + // after returning. // - // The Forwarded, X-Forwarded, X-Forwarded-Host, - // and X-Forwarded-Proto headers are removed from the - // outbound request before Rewrite is called. See also - // the ProxyRequest.SetXForwarded method. + // By default, the X-Forwarded-For header is set to the + // value of the client IP address. If an X-Forwarded-For + // header already exists, the client IP is appended to the + // existing values. As a special case, if the header + // exists in the Request.Header map but has a nil value + // (such as when set by the Director func), the X-Forwarded-For + // header is not modified. // - // Unparsable query parameters are removed from the - // outbound request before Rewrite is called. - // The Rewrite function may copy the inbound URL's - // RawQuery to the outbound URL to preserve the original - // parameter string. Note that this can lead to security - // issues if the proxy's interpretation of query parameters - // does not match that of the downstream server. + // To prevent IP spoofing, be sure to delete any pre-existing + // X-Forwarded-For header coming from the client or + // an untrusted proxy. + // + // Hop-by-hop headers are removed from the request after + // Director returns, which can remove headers added by + // Director. Use a Rewrite function instead to ensure + // modifications to the request are preserved. + // + // Unparsable query parameters are removed from the outbound + // request if Request.Form is set after Director returns. // // At most one of Rewrite or Director may be set. - Rewrite func(*ProxyRequest) + Director func(*http.Request) // The transport used to perform proxy requests. // If nil, http.DefaultTransport is used. @@ -106,13 +115,6 @@ type ReverseProxy struct { ServeHTTP http.HandlerFunc } -// A BufferPool is an interface for getting and returning temporary -// byte slices for use by [io.CopyBuffer]. -type BufferPool interface { - Get() []byte - Put([]byte) -} - func singleJoiningSlash(a, b string) string { aslash := strings.HasSuffix(a, "/") bslash := strings.HasPrefix(b, "/") @@ -169,10 +171,14 @@ func joinURLPath(a, b *url.URL) (path, rawpath string) { // func NewReverseProxy(target *url.URL, transport http.RoundTripper) *ReverseProxy { + if transport == nil { + panic("nil transport") + } rp := &ReverseProxy{ - Rewrite: func(pr *ProxyRequest) { - rewriteRequestURL(pr.Out, target) - }, Transport: transport, + Director: func(req *http.Request) { + rewriteRequestURL(req, target) + }, + Transport: transport, } rp.ServeHTTP = rp.serveHTTP return rp @@ -190,6 +196,14 @@ func rewriteRequestURL(req *http.Request, target *url.URL) { } } +func copyHeader(dst, src http.Header) { + for k, vv := range src { + for _, v := range vv { + dst.Add(k, v) + } + } +} + // Hop-by-hop headers. These are removed when sent to the backend. // As of RFC 7230, hop-by-hop headers are required to appear in the // Connection header field. These are the headers defined by the @@ -207,14 +221,6 @@ var hopHeaders = []string{ "Upgrade", } -func copyHeader(dst, src http.Header) { - for k, vv := range src { - for _, v := range vv { - dst.Add(k, v) - } - } -} - func (p *ReverseProxy) errorHandler(rw http.ResponseWriter, r *http.Request, err error, writeHeader bool) { logger.Errorf("http proxy to %s failed: %s", r.URL.String(), err) if writeHeader { @@ -282,6 +288,7 @@ func (p *ReverseProxy) serveHTTP(rw http.ResponseWriter, req *http.Request) { outreq.Header = make(http.Header) // Issue 33142: historical behavior was to always allocate } + p.Director(outreq) outreq.Close = false reqUpType := UpgradeType(outreq.Header) @@ -313,12 +320,19 @@ func (p *ReverseProxy) serveHTTP(rw http.ResponseWriter, req *http.Request) { // outreq.Header.Del("X-Forwarded-Host") // outreq.Header.Del("X-Forwarded-Proto") - pr := &ProxyRequest{ - In: req, - Out: outreq, + if clientIP, _, err := net.SplitHostPort(req.RemoteAddr); err == nil { + // If we aren't the first proxy retain prior + // X-Forwarded-For information as a comma+space + // separated list and fold multiple headers into one. + prior, ok := outreq.Header["X-Forwarded-For"] + omit := ok && prior == nil // Issue 38079: nil now means don't populate the header + if len(prior) > 0 { + clientIP = strings.Join(prior, ", ") + ", " + clientIP + } + if !omit { + outreq.Header.Set("X-Forwarded-For", clientIP) + } } - p.Rewrite(pr) - outreq = pr.Out if _, ok := outreq.Header["User-Agent"]; !ok { // If the outbound request doesn't have a User-Agent header set, @@ -326,15 +340,21 @@ func (p *ReverseProxy) serveHTTP(rw http.ResponseWriter, req *http.Request) { outreq.Header.Set("User-Agent", "") } + var ( + roundTripMutex sync.Mutex + roundTripDone bool + ) trace := &httptrace.ClientTrace{ Got1xxResponse: func(code int, header textproto.MIMEHeader) error { - h := rw.Header() - // copyHeader(h, http.Header(header)) - for k, vv := range header { - for _, v := range vv { - h.Add(k, v) - } + roundTripMutex.Lock() + defer roundTripMutex.Unlock() + if roundTripDone { + // If RoundTrip has returned, don't try to further modify + // the ResponseWriter's header map. + return nil } + h := rw.Header() + copyHeader(h, http.Header(header)) rw.WriteHeader(code) // Clear headers, it's not automatically done by ResponseWriter.WriteHeader() for 1xx responses @@ -345,6 +365,9 @@ func (p *ReverseProxy) serveHTTP(rw http.ResponseWriter, req *http.Request) { outreq = outreq.WithContext(httptrace.WithClientTrace(outreq.Context(), trace)) res, err := transport.RoundTrip(outreq) + roundTripMutex.Lock() + roundTripDone = true + roundTripMutex.Unlock() if err != nil { p.errorHandler(rw, outreq, err, false) errMsg := err.Error() @@ -371,6 +394,8 @@ func (p *ReverseProxy) serveHTTP(rw http.ResponseWriter, req *http.Request) { return } + RemoveHopByHopHeaders(res.Header) + if !p.modifyResponse(rw, res, outreq) { return }