Nginx as reverse proxy and IP resolution

We know how reverse proxies work and how it masks the backend server. If needed, this blog explains it beautifully. NginX, is a web server that can also be used as a reverse proxy, load balancer, mail proxy and HTTP cache. In my application, I used it to act as a reverse proxy. One of the other uses we made was to avoid the CORS behavior. Since we are too lazy to add the extra headers and handling in the backend (Just kidding!!). We did not want those extra OPTIONS calls for each request was among the other reasons.

How did we avoid CORS using NginX?

Well, since any call from the UI app goes to the container with Nginx before going to the server, we have the control on how or where we want to send the request.

worker_processes 1;

events { worker_connections 1024; }


http {
    upstream myserveraddress {
        server test:80;
    }

    server {
        listen 8080;
        access_log /var/log/nginx/access.log compression;

        location /api {
            proxy_pass         http://randomserverurl/;
            proxy_redirect     off;
            proxy_set_header   Host $host;
            proxy_set_header   X-Real-IP $remote_addr;
            proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header   X-Forwarded-Host $server_name;
        }
    }
}

The UI container has the NginX running it.

Result: The UI makes the api call on the same origin, therefore no CORS issue. The nginx rule “/api” takes care of proxying the request to the server with the headers and cookie as it is. proxy_pass does the trick.

Ofcourse some of the headers get changed like Forwarded, RealIP header, Host etc. But just adding few lines can help you to get that as well on the server side. But rest remains the same.

So now whats the problem here?

The request will be passed on or proxied to the server from the nginx. Therefore, will give the feeling of server and web UI app to be on the same instance.

Now, on production, we generally make use of ALBs to redirect or pass on our requests to the corresponding containers. The same case lies here, the request goes through the ALB first.

Fine. So what? My request will be proxied, it will go to the ALB and later to the corresponding container IP.

Okay! What about if there are more than one instance of the server running? We generally have that for Production environments right? Then how does the NginX decide which instance to send the request to? To answer this, NginX doesn’t.

We face issues where after sometime, the API cannot go to the server proxied to and therefore may return 504 or timeout issues.

The reason for this could be anything, may be the ip which the request was proxied to has changed. The instance is busy and cannot take more requests. Generally an ALB takes care of this if no proxy is involved. Since it knows which instance and how to divide the requests.

Now why couldn’t NginX do it?

Hmm, the simple reason is NginX resolves the IP and caches it when the container is created or brought up. Thats the reason, restarting the server should fix the issue.

Permanent Solution

What if we tell NginX, Hey! we will give you the DNS IP (resolver) and use it to resolve it after some intervals and this way the updated IP will be communicated to you. Peace!!!

Enough talk. Lets do it!

location /api {
	#   Since the api calls proxy passed to ALBs,
	#   therefore we need to resolve ip after some interval
	        resolver 172.250.166.56:50 valid=10s;
  	        resolver_timeout 10s;
            set $serverurl "http://randomserverurl";
    	    proxy_pass         $serverurl;
            proxy_redirect     off;
            proxy_set_header   Host $host;
            proxy_set_header   X-Real-IP $remote_addr;
            proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header   X-Forwarded-Host $server_name;
        }

*resolver_timeout tells nginx how long to wait for the response from DNS

setting the url in avariable makes sure that the ip is resolved whenever the requestis made and validity is preserved.

The valid flag means how long NGINX will consider answer from resolver as valid and will not ask resolver for that period. If theNginx resolves the IP successfully, it will not use resolver for the timeperiod.

And Done!! Now the Nginx will take care of resolving the IP just like an ALB can do. And our job is done.

Tagged : / / / / / / / / /

Simple and Powerful ReverseProxy in Go

In this article we will learn about reverse proxy, where to use it and how to implement it in Golang.

A reverse proxy is a server that sits in front of web servers and forwards client (e.g. web browser) requests to web servers. They give you control over the request from clients and responses from the servers and then we can use that to leverage benefits like caching, increased security and many more.

Before we learn more about reverse proxy, lets quickly understand the difference between a normal proxy (aka forward proxy) and reverse proxy.

In Forward Proxy, proxy retrieves data from another website on the behalf of original client. It sits in front of a client (your browser) and ensures that no backend server ever communicates directly with the client. All the client requests go through the forward proxy and hence the server only communicates with that proxy (assuming proxy is its client). In this case, the proxy masks the client.

Forward Proxy Flow

On the other hand, a Reverse Proxy sits in front of backend servers and ensures that no client ever communicates directly with the servers. All the client requests go to server via reverse proxy and hence client is always communicating to reverse proxy and never with the actual server. In this case, the proxy masks the backend servers. Few examples of reverse proxy are Nginx Reverse proxy, HAProxy.

Reverse Proxy Flow

Reverse Proxy Use cases

Load balancing: a reverse proxy can provide a load balancing solution which will distribute the incoming traffic evenly among the different servers to prevent any single server from becoming overloaded

Preventing security attacks: since the actual web-servers never need to expose their public IP address, attacks such as DDoS can only target the reverse proxy which can be secured with more resources to fend off the cyber attack. Your actual servers are always safe.

Caching: Let’s say your actual servers are in a region far from your users, you can deploy regional reverse proxies which can cache content and serve to local users.

SSL encryption: As SSL communication with each client is computationally expensive, using a reverse proxy it can handle all your SSL related stuff and then freeing up valuable resources on your actual servers.

Golang Implementation

package main

import (
	"log"
	"net/http"
	"net/http/httputil"
	"net/url"
)

// NewProxy takes target host and creates a reverse proxy
func NewProxy(targetHost string) (*httputil.ReverseProxy, error) {
	url, err := url.Parse(targetHost)
	if err != nil {
		return nil, err
	}

	return httputil.NewSingleHostReverseProxy(url), nil
}

// ProxyRequestHandler handles the http request using proxy
func ProxyRequestHandler(proxy *httputil.ReverseProxy) func(http.ResponseWriter, *http.Request) {
	return func(w http.ResponseWriter, r *http.Request) {
		proxy.ServeHTTP(w, r)
	}
}

func main() {
	// initialize a reverse proxy and pass the actual backend server url here
	proxy, err := NewProxy("http://my-api-server.com")
	if err != nil {
		panic(err)
	}

	// handle all requests to your server using the proxy
	http.HandleFunc("/", ProxyRequestHandler(proxy))
	log.Fatal(http.ListenAndServe(":8080", nil))
}

And yes! Thats all it takes to create a simple reverse proxy in Go. We used standard library “net/http/httputil” and created a single host reverse proxy. Any request to our proxy server is proxied to the backend server located at http://my-api-server.com. The code is pretty much self-explanatory if you are from Go background.

Modifying the response

HttpUtil reverse proxy provides us a very simple mechanism to modify the response we got from the servers. This response can be cached or changed based on your use cases. Let’s see how we can make this change.

// NewProxy takes target host and creates a reverse proxy
func NewProxy(targetHost string) (*httputil.ReverseProxy, error) {
	url, err := url.Parse(targetHost)
	if err != nil {
		return nil, err
	}

	proxy := httputil.NewSingleHostReverseProxy(url)
	proxy.ModifyResponse = modifyResponse()
	return proxy, nil
}

func modifyResponse() func(*http.Response) error {
	return func(resp *http.Response) error {
		resp.Header.Set("X-Proxy", "Magical")
		return nil
	}
}

You can see in modifyResponse method, we are setting a custom header. Similarly you can read the response body, make changes to it, cache it and then set it back for the client.

In ModifyResponse you can also return an error (if you encounter it while processing response) which then will be handled by proxy.ErrorHandler. ErrorHandler is automatically called if you set error inside modifyResponse.

// NewProxy takes target host and creates a reverse proxy
func NewProxy(targetHost string) (*httputil.ReverseProxy, error) {
	url, err := url.Parse(targetHost)
	if err != nil {
		return nil, err
	}

	proxy := httputil.NewSingleHostReverseProxy(url)
	proxy.ModifyResponse = modifyResponse()
	proxy.ErrorHandler = errorHandler()
	return proxy, nil
}

func errorHandler() func(http.ResponseWriter, *http.Request, error) {
	return func(w http.ResponseWriter, req *http.Request, err error) {
		fmt.Printf("Got error while modifying response: %v \n", err)
		return
	}
}

func modifyResponse() func(*http.Response) error {
	return func(resp *http.Response) error {
		return errors.New("response body is invalid")
	}
}

Modifying the request

You can also modify the request before sending it to the server. In below example we are adding a header before sending it to server. Similarly, you can make any changes to the request before sending.

// NewProxy takes target host and creates a reverse proxy
func NewProxy(targetHost string) (*httputil.ReverseProxy, error) {
	url, err := url.Parse(targetHost)
	if err != nil {
		return nil, err
	}

	proxy := httputil.NewSingleHostReverseProxy(url)

	originalDirector := proxy.Director
	proxy.Director = func(req *http.Request) {
		originalDirector(req)
		modifyRequest(req)
	}

	proxy.ModifyResponse = modifyResponse()
	proxy.ErrorHandler = errorHandler()
	return proxy, nil
}

func modifyRequest(req *http.Request) {
	req.Header.Set("X-Proxy", "Simple-Reverse-Proxy")
}

Complete code

package main

import (
	"errors"
	"fmt"
	"log"
	"net/http"
	"net/http/httputil"
	"net/url"
)

// NewProxy takes target host and creates a reverse proxy
func NewProxy(targetHost string) (*httputil.ReverseProxy, error) {
	url, err := url.Parse(targetHost)
	if err != nil {
		return nil, err
	}

	proxy := httputil.NewSingleHostReverseProxy(url)

	originalDirector := proxy.Director
	proxy.Director = func(req *http.Request) {
		originalDirector(req)
		modifyRequest(req)
	}

	proxy.ModifyResponse = modifyResponse()
	proxy.ErrorHandler = errorHandler()
	return proxy, nil
}

func modifyRequest(req *http.Request) {
	req.Header.Set("X-Proxy", "Simple-Reverse-Proxy")
}

func errorHandler() func(http.ResponseWriter, *http.Request, error) {
	return func(w http.ResponseWriter, req *http.Request, err error) {
		fmt.Printf("Got error while modifying response: %v \n", err)
		return
	}
}

func modifyResponse() func(*http.Response) error {
	return func(resp *http.Response) error {
		return errors.New("response body is invalid")
	}
}

// ProxyRequestHandler handles the http request using proxy
func ProxyRequestHandler(proxy *httputil.ReverseProxy) func(http.ResponseWriter, *http.Request) {
	return func(w http.ResponseWriter, r *http.Request) {
		proxy.ServeHTTP(w, r)
	}
}

func main() {
	// initialize a reverse proxy and pass the actual backend server url here
	proxy, err := NewProxy("http://my-api-server.com")
	if err != nil {
		panic(err)
	}

	// handle all requests to your server using the proxy
	http.HandleFunc("/", ProxyRequestHandler(proxy))
	log.Fatal(http.ListenAndServe(":8080", nil))
}

Reverse proxy is very powerful and can be used for multiple use cases as explained above. You can try customising it as per your case and if you face any issues, I will be very happy to help you with that. If you found the article interesting, please share it so that it can reach to other gophers! Thanks a lot for reading.

Tagged : / /
%d bloggers like this: