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

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

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.

Related

Go httputil.ReverseProxy not overriding the Host header

I'm basically trying to write a reverse proxy server so that when I curl localhost:8080/get it proxies the request to https://nghttp2.org/httpbin/get.
Note: the https://nghttp2.org/httpbin/get service listed above is http/2. But this behavior happens with http/1 as well, such as https://httpbin.org/get.
I'm using httputil.ReverseProxy for this and I'm rewriting the URL while customizing the Host header to not to leak the localhost:8080 to the actual backend.
However, the request still hits the backend with Host: localhost:8080 no matter how many times I set it on the header. Similarly, I used mitmproxy to snoop on the request and it looks like the net/http.Client sets the :authority pseudo-header to localhost:8080
Here's my source code:
package main
import (
"log"
"net/http"
"net/http/httputil"
)
func main() {
proxy := &httputil.ReverseProxy{
Transport: roundTripper(rt),
Director: func(req *http.Request) {
req.URL.Scheme = "https"
req.URL.Host = "nghttp2.org"
req.URL.Path = "/httpbin" + req.URL.Path
req.Header.Set("Host", "nghttp2.org") // <--- I set it here first
},
}
log.Fatal(http.ListenAndServe(":8080", proxy))
}
func rt(req *http.Request) (*http.Response, error) {
log.Printf("request received. url=%s", req.URL)
req.Header.Set("Host", "nghttp2.org") // <--- I set it here as well
defer log.Printf("request complete. url=%s", req.URL)
return http.DefaultTransport.RoundTrip(req)
}
// roundTripper makes func signature a http.RoundTripper
type roundTripper func(*http.Request) (*http.Response, error)
func (f roundTripper) RoundTrip(req *http.Request) (*http.Response, error) { return f(req) }
When I query curl localhost:8080/get the request gets proxied to https://nghttp2.org/httpbin/get. The echoed response shows that clearly my directives setting the Host header didn't do anything:
{
"headers": {
"Accept": "*/*",
"Accept-Encoding": "gzip",
"Host": "localhost:8080",
"User-Agent": "curl/7.54.0"
},
"origin": "2601:602:9c02:16c2:fca3:aaab:3914:4a71",
"url": "https://localhost:8080/httpbin/get"
}
mitmproxy snooping also clearly shows that the request was made with :authority pseudo-header set to localhost:8080:
From http.Request docs:
// For server requests, Host specifies the host on which the URL
// is sought. Per RFC 7230, section 5.4, this is either the value
// of the "Host" header or the host name given in the URL itself.
// It may be of the form "host:port". For international domain
// names, Host may be in Punycode or Unicode form. Use
// golang.org/x/net/idna to convert it to either format if
// needed.
// To prevent DNS rebinding attacks, server Handlers should
// validate that the Host header has a value for which the
// Handler considers itself authoritative. The included
// ServeMux supports patterns registered to particular host
// names and thus protects its registered Handlers.
//
// For client requests, Host optionally overrides the Host
// header to send. If empty, the Request.Write method uses
// the value of URL.Host. Host may contain an international
// domain name.
Host string
So the value of URL.Host is only used in case request.Host is empty which is not the case. Setting request.Host should resolve the issue:
req.Host = "nghttp2.org"
Related issue discussed here.

Implementing IP restrictions in Go gin

I'm setting up a small demo app I'd like only accessible from my home IP address for now, and maybe a small set of technical people I'll coordinate and share with.
I looked through the readme here, but couldn't find:
https://github.com/gin-gonic/gin
---what's the canonical, minimal example for how to limit access on an app to only particular IP addresses in gin?
(Also, any reason this is a particularly unsafe idea in 2018?)
Before I answer your question, I would like to say that it would likely be more practical to limit access to the app using firewall rules rather than in the program itself, but I digress.
To answer your question, after looking through the gin godoc reference I found that the context struct contains a ClientIp() method that:
implements a best effort algorithm to return the real client IP, it parses X-Real-IP and X-Forwarded-For in order to work properly with reverse-proxies such us: nginx or haproxy. Use X-Forwarded-For before X-Real-Ip as nginx uses X-Real-Ip with the proxy's IP.
Therefore, if you are set on doing the IP filtering in the app, you could filter based on the value returned by that method.
Using the basic example given on the Github page:
package main
import "github.com/gin-gonic/gin"
var Whitelist []string = []string{"1.2.3.4"}
func main() {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
whitelisted := false
for _, v := range Whitelist {
if v == c.ClientIP() {
whitelisted = true
}
}
if whitelisted {
c.JSON(200, gin.H{
"message": "pong",
})
} else {
c.JSON(403, gin.H{})
}
})
r.Run() // listen and serve on 0.0.0.0:8080
}

How can I get the client IP address and user-agent in Golang gRPC?

I set up a series of gRPC requests and responses which all work fine, but I'm stuck when I try to get the client IP address and user-agent who is calling my gRPC APIs.
I read the Go gRPC documentation and other sources, but didn't find much valuable information. Few of them are talking about gRPC in Golang.
Should I set up a key-value to store the IP address in the context when setting up the gRPC APIs?
In Golang GRPC, you can use
func (UserServicesServer) Login(ctx context.Context, request *sso.LoginRequest) (*sso.LoginResponse, error) {
p, _ := peer.FromContext(ctx)
request.Frontendip = p.Addr.String()
.
.
}
But, do not forget import "google.golang.org/grpc/peer"
For grpc-gateway is used, the client IP address may be retrieved through x-forwarded-for like this:
// Get IP from GRPC context
func GetIP(ctx context.Context) string {
if headers, ok := metadata.FromIncomingContext(ctx); ok {
xForwardFor := headers.Get("x-forwarded-for")
if len(xForwardFor) > 0 && xForwardFor[0] != "" {
ips := strings.Split(xForwardFor[0], ",")
if len(ips) > 0 {
clientIp := ips[0]
return clientIp
}
}
}
return ""
}
In Golang GRPC, context has 3 values
authority
content-type
user-agent
md,ok:=metadata.FromIncomingContext(ctx)
fmt.Printf("%+v%+v",md,ok)

How to Make My Web Server written in Golang to support HTTP/2 Server Push?

My Web Server is Coded in Golang and supports HTTPS. I wish to leverage HTTP/2 Server Push features in the Web Server. The following Link explains how to convert HTTP Server to Support HTTP/2 :-
https://www.ianlewis.org/en/http2-and-go
However, it is not clear how to implement the Server Push notifications in Golang.
- How should I add the Server Push functionality ?
- How do I control, or manage, the documents and files to be Pushed ?
Go 1.7 and older do not support HTTP/2 server push in the standard library. Support for server push will be added in the upcoming 1.8 release (see the release notes, expected release is February).
With Go 1.8 you can use the new http.Pusher interface, which is implemented by net/http's default ResponseWriter. Pushers Push method returns ErrNotSupported, if server push is not supported (HTTP/1) or not allowed (the client has disabled server push).
Example:
package main
import (
"io"
"log"
"net/http"
)
func main() {
http.HandleFunc("/pushed", func(w http.ResponseWriter, r *http.Request) {
io.WriteString(w, "hello server push")
})
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if pusher, ok := w.(http.Pusher); ok {
if err := pusher.Push("/pushed", nil); err != nil {
log.Println("push failed")
}
}
io.WriteString(w, "hello world")
})
http.ListenAndServeTLS(":443", "server.crt", "server.key", nil)
}
If you want to use server push with Go 1.7 or older use can use the golang.org/x/net/http2 and write the frames directly.
As mentioned in other answers, you can make use of Go 1.8 feature (cast the writer to http.Pusher and then use the Push method).
That comes with a caveat: you must be serving the HTTP2 traffic right from your server.
If you're behind a proxy like NGINX, this might not work. If you want to consider that scenario, you can make use of the Link header to advertise the URLs to be pushed.
// In the case of HTTP1.1 we make use of the `Link` header
// to indicate that the client (in our case, NGINX) should
// retrieve a certain URL.
//
// See more at https://www.w3.org/TR/preload/#server-push-http-2.
func handleIndex(w http.ResponseWriter, r *http.Request) {
var err error
if *http2 {
pusher, ok := w.(http.Pusher)
if ok {
must(pusher.Push("/image.svg", nil))
}
} else {
// This ends up taking the effect of a server push
// when interacting directly with NGINX.
w.Header().Add("Link",
"</image.svg>; rel=preload; as=image")
}
w.Header().Add("Content-Type", "text/html")
_, err = w.Write(assets.Index)
must(err)
}
ps.: I wrote more about this here https://ops.tips/blog/nginx-http2-server-push/ if you're interested.

How to get remote client's IPV4 address from request object

I am using go-gin as server and rendering an html using the code like the following
func dashboardHandler(c *gin.Context) {
c.HTML(200, "dashboard", gin.H{
"title": "Dashboard"
})
Along with title I want to pass the remote client's IPV4 address as well. I tried using the following code to get the IP address but for localhost it gives me ::1:56797 as output. My server is running on localhost:8080
ip, port, err := net.SplitHostPort(c.Request.RemoteAddr)
fmt.Println(ip + ":" + port)
if err != nil {
fmt.Println(err.Error())
}
I followed Correct way of getting Client's IP Addresses from http.Request (Golang) for reference. Is there any way I get the IPV4 address from the request?
you can use this function to get the ip and user agent, but it will give a bracket character if you are trying from localhost but if you try from somewhere else it will work.
func GetIPAndUserAgent(r *http.Request) (ip string, user_agent string) {
ip = r.Header.Get("X-Forwarded-For")
if ip == "" {
ip = strings.Split(r.RemoteAddr, ":")[0]
}
user_agent = r.UserAgent()
return ip, user_agent
}

Resources