Why would I want to do this
I have run in to this issue twice.
The first time was with a reverse proxy that lived inside an IPv6 only server network. Client requests come in through NAT46. The source-IP of the request becomes [fixed 96-bit prefix] [32-bit client IPv4 address]. This means that the reverse proxy could always identify the real client IP. I couldn't find a way to set the X-Forwarded-For
header to that address though. I got around it by modifying the backend server.
This time I have a reverse proxy which will run on Google App Engine. Requests hit Google's load balancer first, which adds the X-Forwarded-For
header and forwards the request to my app. I want to modify the request a bit and then pass it to a backend server, which I cannot modify. The back-end needs the original client IP, and can accept it via X-Forwarded-For
(it's authenticated, don't worry). In this case I want to pass the X-Forwarded-For
header from Google's load balencer through unmodified.
The Problem
It seems like there is no way to set X-Forwarded-For
to a value that I chose when using httputil.ReverseProxy
. If I set it (option 1 below) the client address from the TCP connection will be appended. If if I set it to nil
(option 2 below), it is omitted like the documentation suggests, but that's not what I want either.
package main
import (
"log"
"net/http"
"net/http/httputil"
)
func director(req *http.Request) {
// currently running a PHP script that just records headers
host := "bsweb02.bentonvillek12.org"
// who we dial
req.URL.Scheme = "https"
req.URL.Host = host
// host header
req.Host = host
// proof that most headers can be passed through fine
req.Header["Foo"] = []string{"Bar"}
req.Header["X-Forwarded-For"] = []string{"1.2.3.4"} // option 1
//req.Header["X-Forwarded-For"] = nil // option 2
}
func main() {
http.ListenAndServe(":80", &httputil.ReverseProxy{
Director: director,
})
}
Option 1
array(9) {
["Content-Type"]=>
string(0) ""
["Content-Length"]=>
string(1) "0"
["Foo"]=>
string(3) "Bar"
["X-Forwarded-For"]=>
string(18) "1.2.3.4, 127.0.0.1"
["User-Agent"]=>
string(11) "curl/7.81.0"
["Host"]=>
string(26) "bsweb02.bentonvillek12.org"
["Accept-Encoding"]=>
string(4) "gzip"
["Accept"]=>
string(3) "*/*"
["Connection"]=>
string(5) "close"
}
Option 2
array(8) {
["Content-Type"]=>
string(0) ""
["Content-Length"]=>
string(1) "0"
["Foo"]=>
string(3) "Bar"
["User-Agent"]=>
string(11) "curl/7.81.0"
["Host"]=>
string(26) "bsweb02.bentonvillek12.org"
["Accept-Encoding"]=>
string(4) "gzip"
["Accept"]=>
string(3) "*/*"
["Connection"]=>
string(5) "close"
}
CodePudding user response:
I believe you have two options.
1. Implement http.RoundTripper
You implement your own RoundTripper
and re-set X-Forwarded-For
in there. (demonstration)
type MyRoundTripper struct{}
func (t *MyRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
req.Header["X-Forwarded-For"] = []string{"1.2.3.4"}
return http.DefaultTransport.RoundTrip(req)
}
func main() {
http.ListenAndServe(":80", &httputil.ReverseProxy{
Director: director,
Transport: &MyRoundTripper{},
})
}
When the Transport
field isn't set on httputil.ReverseProxy
it falls back to http.DefaultTransport
, so you can fall back to it too after your custom code.
2. Unset req.RemoteAddr
You reset the original request's RemoteAddr
field before invoking the reverse proxy. This field is set by the HTTP server and, when present, triggers the X-Forwarded-For
replacement in the reverse proxy implementation. (demonstration)
func main() {
http.ListenAndServe(":80", func(w http.ResponseWriter, r *http.Request) {
r.RemoteAddr = ""
proxy := &httputil.ReverseProxy{ Director: director }
proxy.ServeHTTP(w, r)
})
}
However this behavior relies on an implementation detail, which may or may not change in the future. I recommend using option 1 instead.