How to set X-Forwarded-For with httputil.ReverseProxy

Issue

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"
}

Solution

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.

Answered By – blackgreen

Answer Checked By – Willingham (GoLangFix Volunteer)

Leave a Reply

Your email address will not be published.