README

SRP

SRP stands for Simple Reverse Proxy. It does what it says on the tin, and not a whole lot more.

It's been used in production to distribute traffic through a ~35 server architecture for 2 years with 100% uptime. YMMV.

Features

  • Proxy requests from a host to one of many backend IPs/ports
  • Automate HTTPS with TLS termination
  • Load balance using a simple algorithm
  • Redirect to and from hosts
  • Check health automatically
  • Retrieve healthy services with an internal API
  • 0-downtime config file reloads

And nothing else.

Installing

go get egt.run/srp/cmd/srp

Then run srp -h for usage help.

Config file format

The config file has two main parts:

  1. Services maps requests to backend services or redirects.
  2. API that restricts access via an IP subnet.
{
	"Services": {
		"www.example.com": {
			"HealthPath": "/health",
			"Backends": [
				"127.0.0.1:3001",
				"127.0.0.1:3002",
				"127.0.0.1:3003",
				"127.0.0.1:3004",
				"127.0.0.1:3005"
			]
		},
		"example.com": {
			"Redirect": {
				"URL": "https://www.example.com",
				"DiscardPath": true,
				"Permanent": true
			}
		}
	},
	"API": {
		"Subnet": "10.0.0.0/24"
	}
}

Automatic healthchecks

If you provide a HealthPath in the config file, SRP will check the health of your servers every few seconds and stop sending requests to any that fail until the health checks start succeeding. Additionally, if any single request fails, SRP will try that same request again using a different backend (3 tries max).

API

SRP includes a simple API to retrieve each services' healthy backends. Combined with something like egt.run/lanhttp, the API enables your apps to communicate over an internal network, rather than through the public internet, without re-configuring your servers or modifying DNS.

By default the API is disabled. When configured with Subnet, the API responds to /services over the appropriate subnet with JSON resembling the following:

{
	"www.example.internal": {
		"HealthPath": "/health",
		"Backends": [
			"10.0.0.1:3000",
			"10.0.0.2:3000"
		]
	}
}

Only the healthy IPs will be returned in the API.

lanhttp or similar can help you periodically call this API to update healthy IPs and route *.internal traffic directly to the live IPs, skipping SRP entirely, to keep chatty internal networks from impacting the performance of SRP.

Why build SRP?

Complexity doesn't belong in the infrastructure layer. When something goes wrong at this level, it can be catastrophic. You need to diagnose the issue quickly and deliver a fix in minutes to meet your SLAs. A small, simple and well-tested codebase is the only way to achieve that.

HAProxy, Nginx, and Apache are very complex, which is far beyond the need of most projects. Several new Go-based reverse proxies that use autocert, such as Traefik and Caddy, are very large and complex as well, with plenty of opportunity for bugs.

Instead, SRP keeps it simple. There's a much smaller surface for bugs. It's easier and faster to debug if issues occur (especially nice when they occur in production and you need to roll out a fix quickly).

Expand ▾ Collapse ▴

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Pledge

func Pledge()

Pledge is not supported outside of OpenBSD.

func Unveil

func Unveil(filename, perm string)

Unveil is not supported outside of OpenBSD.

func UnveilBlock

func UnveilBlock()

UnveilBlock is not supported outside of OpenBSD.

Types

type Logger

type Logger interface {
	Printf(format string, vals ...interface{})
	ReqPrintf(reqID, format string, vals ...interface{})
}

Logger logs error messages for the caller where those errors don't require returning, i.e. the logging itself constitutes handling the error.

type Registry

type Registry struct {
	// Services maps hostnames to one of the following:
	//
	// * IPs with optional healthcheck paths, OR
	// * A redirect to another hostname
	Services map[string]*backend

	// API restricts internal API access to a subnet, which should be an
	// private LAN.
	API struct{ Subnet string }
}

Registry maps hosts to backends with other helpful info, such as healthchecks.

func NewRegistry

func NewRegistry(filename string) (*Registry, error)

NewRegistry for a given configuration file. This reports an error if any frontend host has no backends.

func (*Registry) Hosts

func (r *Registry) Hosts() []string

Hosts for the registry suitable for generating HTTPS certificates. This automatically removes *.internal domains.

type ReverseProxy

type ReverseProxy struct {
	// contains filtered or unexported fields
}

ReverseProxy maps frontend hosts to backends. If HealthPath is set in the config.json file, ReverseProxy checks the health of backend servers periodically and automatically removes them from rotation until health checks pass.

func NewProxy

func NewProxy(log Logger, reg *Registry, timeout time.Duration) *ReverseProxy

NewProxy from a given Registry.

func (*ReverseProxy) CheckHealth

func (r *ReverseProxy) CheckHealth() error

CheckHealth of backend servers in the registry concurrently, and update the registry so requests are only routed to healthy servers.

func (*ReverseProxy) RedirectHTTPHandler

func (rp *ReverseProxy) RedirectHTTPHandler() (http.Handler, error)

RedirectHTTPHandler redirects http requests to use the API if the request originated from the whitelisted subnet. In all other GET and HEAD requests, this handler redirects to HTTPS. For POST, PUT, etc. this handler throws an error letting the client know to use HTTPS.

func (*ReverseProxy) ServeHTTP

func (r *ReverseProxy) ServeHTTP(w http.ResponseWriter, req *http.Request)

func (*ReverseProxy) UpdateRegistry

func (r *ReverseProxy) UpdateRegistry(reg *Registry)

UpdateRegistry for the reverse proxy with new frontends, backends, and health checks.

Directories

Path Synopsis
cmd/srp