DEV Community

Cover image for Build reverse proxy server in Go
b0r
b0r

Posted on

Build reverse proxy server in Go

Learn how to build a simple reverse proxy server in Go.

Table of Contents:

  1. What is a Proxy Server
  2. Proxy Server types
    1. Forward Proxy
    2. Reverse Proxy
  3. Reverse Proxy Implementation
    1. Step 1: Create origin server
    2. Step 2: Create a reverse proxy server
    3. Step 3: Forward a client request to the origin server (via reverse proxy)
    4. Step 4: Copy origin server response to the client (via reverse proxy)
  4. Common errors
  5. Conclusion

What is a Proxy Server

Proxy server is a server that provides a gateway between the client (Alice) and the origin server (Bob). Instead of connecting directly to the origin server (to fulfill a resource request), client directs the request to the proxy server, which evaluates the request and performs the required action on origin server on the client behalf (e.g. get current time). [1]

Proxy concept

By H2g2bob - Own work, CC0, https://commons.wikimedia.org/w/index.php?curid=16337577

Proxy Server types

Forward Proxy

An ordinary forward proxy is an intermediate server that sits between the client and the origin server. In order to get content from the origin server, the client sends a request to the proxy naming the origin server as the target and the proxy then requests the content from the origin server and returns it to the client. The origin server is only aware of the proxy server and not the client (optional).

Use cases

A typical usage of a forward proxy is to provide Internet access to internal clients that are otherwise restricted by a firewall.
The forward proxy can also use caching to reduce network usage.
In addition, forward proxy can also be used to hide clients IP address. [2]

Reverse Proxy

A reverse proxy, by contrast, appears to the client just like an ordinary web server. No special configuration on the client is necessary. The client makes ordinary requests for content in the name-space of the reverse proxy. The reverse proxy then decides where to send those requests, and returns the content as if it was itself the origin.

Use cases

A typical usage of a reverse proxy is to provide Internet users access to a server that is behind a firewall (opposite of Forward Proxy).

Reverse proxies can also be used to balance load among several back-end servers, or to filter, rate limit or log incoming request, or to provide caching for a slower back-end server.

In addition, reverse proxies can be used simply to bring several servers into the same URL space. [2]

Reverse Proxy Implementation

Step 1: Create origin server

In order to test our reverse proxy, we first need to create and start a simple origin server.
Origin server will be started at port 8081 and it will return a string containing the value "origin server response".

  package main

  import (
    "fmt"
    "log"
    "net/http"
    "time"
  )

    func main() {
      originServerHandler := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
        fmt.Printf("[origin server] received request at: %s\n", time.Now())
        _, _ = fmt.Fprint(rw, "origin server response")
        })

      log.Fatal(http.ListenAndServe(":8081", originServerHandler))
    }
Enter fullscreen mode Exit fullscreen mode
Step 1 Test

Start the server:

go run main
Enter fullscreen mode Exit fullscreen mode

Use curl command to validate origin server works as expected:

% curl -i localhost:8081
HTTP/1.1 200 OK
Date: Tue, 07 Dec 2021 19:32:00 GMT
Content-Length: 22
Content-Type: text/plain; charset=utf-8

origin server response%
Enter fullscreen mode Exit fullscreen mode

In the terminal of the origin server you should see:

[origin server] received request at: 2021-12-07 20:08:44.302807 +0100 CET m=+518.629994085
Enter fullscreen mode Exit fullscreen mode

Step 2: Create a reverse proxy server

package main

import (
    "fmt"
    "log"
    "net/http"
)

func main() {

    reverseProxy := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
        fmt.Printf("[reverse proxy server] received request at: %s\n", time.Now())
    })

    log.Fatal(http.ListenAndServe(":8080", reverseProxy))
}
Enter fullscreen mode Exit fullscreen mode
Step 2 Test

Start the server:

go run main
Enter fullscreen mode Exit fullscreen mode

Use curl command to validate reverse proxy works as expected:

% curl -i localhost:8080
HTTP/1.1 200 OK
Date: Tue, 07 Dec 2021 19:17:45 GMT
Content-Length: 0
Enter fullscreen mode Exit fullscreen mode

In the terminal of the reverse proxy server you should see:

[reverse proxy server] received request: 2021-12-07 20:09:44.302807 +0100 CET m=+518.629994085
Enter fullscreen mode Exit fullscreen mode

Step 3: Forward a client request to the origin server (via reverse proxy)

Next, we will update reverse proxy to forward a client request to the origin server. Update main function as follows:

func main() {

  // define origin server URL
    originServerURL, err := url.Parse("http://127.0.0.1:8081")
    if err != nil {
        log.Fatal("invalid origin server URL")
    }

    reverseProxy := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
        fmt.Printf("[reverse proxy server] received request at: %s\n", time.Now())

    // set req Host, URL and Request URI to forward a request to the origin server
        req.Host = originServerURL.Host
        req.URL.Host = originServerURL.Host
        req.URL.Scheme = originServerURL.Scheme
        req.RequestURI = ""

    // send a request to the origin server
        _, err := http.DefaultClient.Do(req)
        if err != nil {
            rw.WriteHeader(http.StatusInternalServerError)
            _, _ = fmt.Fprint(rw, err)
            return
        }
    })

    log.Fatal(http.ListenAndServe(":8080", reverseProxy))
}
Enter fullscreen mode Exit fullscreen mode
Step 3 Test

Start the server:

go run main
Enter fullscreen mode Exit fullscreen mode

Use curl command to validate reverse proxy works as expected:

% curl -i localhost:8080
HTTP/1.1 200 OK
Date: Tue, 07 Dec 2021 19:35:19 GMT
Content-Length: 0
Enter fullscreen mode Exit fullscreen mode

In the terminal of the reverse proxy server you should see:

[reverse proxy server] received request at: 2021-12-07 20:37:30.288783 +0100 CET m=+4.150788001
Enter fullscreen mode Exit fullscreen mode

In the terminal of the origin server you should see:

received request: 2021-12-07 20:37:30.290371 +0100 CET m=+97.775715418
Enter fullscreen mode Exit fullscreen mode

Step 4: Copy Origin Server Response

Once we were able to proxy a client response to the origin server, we need to get the response from the origin server back to the client. To do that, update the main function as follows:


    func main() {

    // define origin server URL
    originServerURL, err := url.Parse("http://127.0.0.1:8081")
    if err != nil {
        log.Fatal("invalid origin server URL")
    }

    reverseProxy := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
        fmt.Printf("[reverse proxy server] received request at: %s\n", time.Now())

        // set req Host, URL and Request URI to forward a request to the origin server
        req.Host = originServerURL.Host
        req.URL.Host = originServerURL.Host
        req.URL.Scheme = originServerURL.Scheme
        req.RequestURI = ""

      // save the response from the origin server
        originServerResponse, err := http.DefaultClient.Do(req)
        if err != nil {
            rw.WriteHeader(http.StatusInternalServerError)
            _, _ = fmt.Fprint(rw, err)
            return
        }

      // return response to the client
        rw.WriteHeader(http.StatusOK)
        io.Copy(rw, originServerResponse.Body)
    })

    log.Fatal(http.ListenAndServe(":8080", reverseProxy))

Enter fullscreen mode Exit fullscreen mode
Step 4 Test

Start the server:

go run main
Enter fullscreen mode Exit fullscreen mode

Use curl command to validate reverse proxy works as expected:

% curl -i localhost:8080
HTTP/1.1 200 OK
Date: Tue, 07 Dec 2021 19:42:07 GMT
Content-Length: 22
Content-Type: text/plain; charset=utf-8

origin server response%
Enter fullscreen mode Exit fullscreen mode

In the terminal of the reverse proxy server you should see:

[reverse proxy server] received request at: 2021-12-07 20:42:07.654365 +0100 CET m=+5.612744376
Enter fullscreen mode Exit fullscreen mode

In the terminal of the origin server you should see:

received request: 2021-12-07 20:42:07.657175 +0100 CET m=+375.150991460
Enter fullscreen mode Exit fullscreen mode

Common errors

  1. Request.RequestURI can't be set in client request

If the request is executed to the http://localhost:8080/what-is-this the RequestURI value will be what-is-this.

According to Go docs ../src/net/http/client.go:217, requestURI should always be set to "" before trying to send a request.

Conclusion

In this article, reverse proxy explanation and its use cases were described. In addition, simple implementation of the reverse proxy in Go was provided.

Readers are encouraged to try improve this example by implementing copying of the response headers, adding X-Forwarded-For header and to implement HTTP2 support.

Also, don't forget to watch this awesome talk FOSDEM 2019: How to write a reverse proxy with Go in 25 minutes. by Julien Salleyron on YT.

Resources:

[1] https://en.wikipedia.org/wiki/Proxy_server#cite_note-apache-forward-reverse-5
[2] https://httpd.apache.org/docs/2.0/mod/mod_proxy.html#forwardreverse
[3] Photo by Clayton on Unsplash

Latest comments (1)

Collapse
 
skyris profile image
Victor Klimov • Edited

Thanks for this tutorial. One tiny suggestion. We don't have to hardcode our target address if we make little change:
originServerURL, err := url.Parse(req.RequestURI)
if err != nil {
log.Fatal("invalid origin server URL")
}

and move this code inside handler
Then we can use curl this way:
curl --proxy http://localhost:8080 http://localhost:8081/some-address
or via env variable:
HTTP_PROXY=http://localhost:8080
export HTTP_PROXY
curl http://localhost:8081/some-address