Writing your own Reverse Proxy server using Golang

5 minute read

Writing a Reverse Proxy server in Go is a matter of single digit lines of code due to its rock solid standard library and its low level network plumbing capabilities. Recently I came across some use cases where I needed to write my own Reverse Proxy server and of course Go was the pragmatic choice.

reverse proxy

Let us first start with defining Reverse Proxy and its common uses.

Reverse Proxy

A Reverse Proxy is an intermediary server that sits between multiple clients and servers, and directs client requests to appropriate backend server. It is commonly used for:

  • Load Balancing – It can distribute the load across multiple available backend servers in such manner that maximizes speed and capacity utilization while ensuring no one server is overloaded, which can degrade performance.

  • Security – By intercepting requests headed for your backend servers, a reverse proxy server protects their identities and acts as an additional defense against security attacks.

  • Caching – It can cache content to serve, and you can deploy regional proxies to cache content related to specific regions.

  • TLS Termination – We can use the reverse proxy to terminate the SSL/TLS connection with the clients in cases where backend servers are being hosted on the same secure VPC network, and it’s ok to let the reverse proxy <-> backend server connection insecure.

  • More – Literally, there are a lot more use cases that can take leverage of reverse proxies, that I leave for you to explore.

Personal Use Cases

Some common use cases I personally use reverse proxy server for are:

  • TLS Termination – Lets say I have multiple servers running on the same VPC network, each has it’s own copy of the TLS certificate issued to same domain name, it makes sense to let the reverse proxy handle the TLS handshake and terminate the secure connection and forward it insecurely to appropriate backend server on the same VPC isolated from public reach, now instead of having TLS certs on every backend server you can have them on proxy only.

  • Single VPS Dev Environment – I have multiple backend servers for a single project, each with separate responsibilities, that I need to deploy but the servers would sit ideal 99% of the time I don’t want to spin up multiple VPS for each for a dev environment, which would cost me money, also I don’t want to explicitly mention the port numbers on the client side to have the servers run on the same VPS. To solve this, I can run all the backend servers and reverse proxy on a single VPS, and have the requests proxied to servers running on different ports.

Also sometimes for Authorization, Caching or implementing a feature like Request Limit per client.

Let’s Code

Enough with the uses, lets jump over to implementation. You are probably already familiar with Go standard library’s net/http package, there’s also a utility package for it you might have or might not have used, that is net/http/httputil. We’ll be using this utility package because it has the type ReverseProxy defined which makes writing a reverse proxy so simple using Go.

Conditions

Let’s define some condition based on which we’ll be forwarding the requests to appropriate server. For this tutorial lets stick to those conditions being the domain name or host of the requests:

  1. If request came on host sub1.xyz.com, forward to localhost:8080.
  2. If request came on host sub2.xyz.com, forward to localhost:8181.
  3. If request came from an unknown host, forward to localhost:8888.

main.go

Now lets create a main.go file, which will do the following:

 1    package main
 2
 3    import "net/http"
 4
 5    func main() {
 6        http.HandleFunc("/", proxy)
 7        if err := http.ListenAndServe(":4433", nil); err != nil {
 8            panic(err)
 9        }
10    }
11
12    func proxy(w http.ResponseWriter, r *http.Request) {
13        // We will get to this later ...
14    }

Now that we have a basic server in place, let’s write our conditions for reverse proxy and use it to forward requests inside our proxy handler.

 1    package main
 2
 3    import (
 4        "net/http"
 5        "net/http/httputil"
 6        "net/url"
 7        "strings"
 8    )
 9
10    func main() {
11        http.HandleFunc("/", proxy)
12        if err := http.ListenAndServe(":4433", nil); err != nil {
13            panic(err)
14        }
15    }
16
17    func proxy(w http.ResponseWriter, r *http.Request) {
18        if strings.HasSuffix(r.Host, "://sub1.xyz.com") { // forward requests from sub1.xyz.com to localhost:8080
19            if url, err := url.Parse("http://localhost:8080"); err == nil {
20                proxy := httputil.NewSingleHostReverseProxy(url)
21                proxy.ServeHTTP(w, r)
22            }
23        } else if strings.HasSuffix(r.Host, "://sub2.xyz.com") { // forward requests from sub2.xyz.com to localhost:8181
24            if url, err := url.Parse("http://localhost:8181"); err == nil {
25                proxy := httputil.NewSingleHostReverseProxy(url)
26                proxy.ServeHTTP(w, r)
27            }
28        } else { // forward requests from unknown host to localhost:8888
29            if url, err := url.Parse("http://localhost:8888"); err == nil {
30                proxy := httputil.NewSingleHostReverseProxy(url)
31                proxy.ServeHTTP(w, r)
32            }
33        }
34    }

The httputil’s NewSingleHostReverseProxy(url) function returns a new ReverseProxy that routes requests to url provided. And that’s all, or what minimal we need to run a reverse proxy based on conditions we defined.

But there are many ways we can refactor this code, starting from using predefined proxy objects instead of creating a new one on each request. Or I prefer a map map[string]*httputil.ReverseProxy which maps hosts to proxies, so we can use the map to check if a proxy exists for host then use it, else use the default one. I’ll leave the refactoring to anyone following this tutorial to keep it short.

Next Steps

Though it is pretty simple to write a reverse proxy using Go’s standard library, but it’s missing a lot of good stuff that comes with nginx or other solutions available. So you can start by implementing some of them into your own reverse proxy that is if the available solutions aren’t feasible and you are having to develop your own, like:

  • Caching
  • Logging
  • Protocol Support (HTTP, HTTPS, HTTP/1.1, HTTP/2, …)
  • Rate Limit
  • TLS Support
  • Load Balancing