A zero-configuration reverse proxy.
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:
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.
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)
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.
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 is available on GitHub.
Since this library was thrown together in a couple days and isn't particularly well tested, you should probably use nginx or haproxy instead.