Writing your own Reverse Proxy server using Golang
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.
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 thereverse 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 havingTLS 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 andreverse 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:
- If request came on host
sub1.xyz.com
, forward tolocalhost:8080
. - If request came on host
sub2.xyz.com
, forward tolocalhost:8181
. - 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
- …