enables add-x-forwarded by default, added hide-x-forwarded

This commit is contained in:
yusing 2024-09-30 16:16:56 +08:00
parent 9065d990e5
commit ebedbc931f
4 changed files with 179 additions and 72 deletions

View file

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

View file

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

View file

@ -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")
},
}

View file

@ -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 {
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))
for k, vv := range header {
for _, v := range vv {
h.Add(k, v)
}
}
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
}