socketmaster

A zero-configuration reverse proxy.

Overview

socketmaster is a reverse proxy written in Go. It listens on a local network port for downstream services which configure upstream sockets on-demand rather than via a config file.

It's designed to solve two problems:

  1. Running multiple applications which need to bind the same port but only receive the traffic that was meant for them. For example: api.example.com should go to the api service and www.example.com should go to the www service, even though both services are running on the same port (443 or 80) on the same machine.
  2. Restarting an application without severing the upstream connection. For example if we redeploy the api service a request may take a little longer, but otherwise the user would have no idea a deployment happened. (They won't see a 404 or 500 error)

Design

socketmaster listens on a control socket (it defaults to 127.0.0.1:9999) for downstream connections. When one comes in there's a custom handshake that occurs where the downstream client tells the socket master which port to listen on:

li, err := client.Listen(protocol.SocketDefinition{
	Port: 443,
	HTTP: &protocol.SocketHTTPDefinition{
		DomainSuffix: "example.com",
	},
})
http.Serve(li, nil)

In this example all https traffic for example.com would be forwarded to this Go program and served by Go's default http server.

The socket master creates (or reuses) an upstream listener on the requested port and starts forwarding traffic to the service. The downstream connection is converted into a net.Listener with the yamux library and all upstream connections are multiplexed onto a single downstream connection.

When a downstream connection is severed the socket master removes it from it's routing pool. The upstream listener is kept alive for 30 seconds and connections are queued up. If the service comes back in that time, then those connections will be resolved. If not they will be closed.

Except for the control port, all configuration is done on-demand. There are no files to manage, and other than starting the socket master beforehand, no coordination is required for deployments.

Usage

Install Go, then run:

go get github.com/badgerodon/socketmaster/...

This will create a socketmaster command in GOPATH/bin. Run it:

socketmaster

Now create an a.go file anywhere you'd like (perhaps in /tmp/a.go):

package main

import (
	"fmt"
	"io"
	"net/http"
	"os"

	"github.com/badgerodon/socketmaster/client"
	"github.com/badgerodon/socketmaster/protocol"
)

func main() {
	li, err := client.Listen(protocol.SocketDefinition{
		Port: 8000,
		HTTP: &protocol.SocketHTTPDefinition{
			PathPrefix: "/a",
		},
	})
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
	defer li.Close()

	http.HandleFunc("/", func(res http.ResponseWriter, req *http.Request) {
		io.WriteString(res, "From A")
	})
	err = http.Serve(li, nil)
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
}

Run it (in another terminal):

go run a.go

And localhost:8000/a/ should now be accessible (once again in another terminal):

curl localhost:8000/a/

This is hitting the socket master, which is then proxying the request to a.go. Let's add a b.go (very much like a.go) to illustrate how multiple services can listen on the same port:

package main

import (
	"fmt"
	"io"
	"net/http"
	"os"

	"github.com/badgerodon/socketmaster/client"
	"github.com/badgerodon/socketmaster/protocol"
)

func main() {
	li, err := client.Listen(protocol.SocketDefinition{
		Port: 8000,
		HTTP: &protocol.SocketHTTPDefinition{
			PathPrefix: "/b",
		},
	})
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
	defer li.Close()

	http.HandleFunc("/", func(res http.ResponseWriter, req *http.Request) {
		io.WriteString(res, "From B")
	})
	err = http.Serve(li, nil)
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
}

/a/ gives you From A now, and /b/ gives you From B:

curl localhost:8000/a/
curl localhost:8000/b/

You can also route based on DomainSufix, or not use HTTP at all (in which case the socket master merely copies the data in both directions). The socket master can also terminate TLS connections:

li, err := client.Listen(protocol.SocketDefinition{
	Port: 443,
	TLS: &protocol.SocketTLSDefinition{
		Cert: tlsCert,
		Key:  tlsKey,
	},
})

TLS.Cert and TLS.Key are PEM encoded strings. For an example just run:

cat ~/.ssh/id_rsa

Source Code

Source code is available on GitHub.

Caveats

Since this library was thrown together in a couple days and isn't particularly well tested, you should probably use nginx or haproxy instead.