Skip to content

Commit

Permalink
Merge pull request #226 from square/cs/socket-activation
Browse files Browse the repository at this point in the history
Add systemd socket activation support
  • Loading branch information
csstaub authored Jun 17, 2019
2 parents ee45a9f + 1d80434 commit e599171
Show file tree
Hide file tree
Showing 53 changed files with 15,812 additions and 183 deletions.
2 changes: 1 addition & 1 deletion Dockerfile-test
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ MAINTAINER Cedric Staub "cs@squareup.com"

# Install build dependencies
RUN apt-get update && \
apt-get install --yes build-essential libtool python3.5 netcat softhsm2 rsyslog && \
apt-get install --yes build-essential libtool python3.5 netcat softhsm2 rsyslog systemd && \
mkdir -p /etc/softhsm /var/lib/softhsm/tokens /go/src/github.com/square/ghostunnel && \
ln -s /usr/bin/python3.5 /usr/bin/python3 && \
go get github.com/wadey/gocovmerge && \
Expand Down
8 changes: 2 additions & 6 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ ghostunnel.man: ghostunnel

# Test binary with coverage instrumentation
ghostunnel.test: $(SOURCE_FILES)
go test -c -covermode=count -coverpkg .,./auth,./certloader,./proxy,./wildcard
go test -c -covermode=count -coverpkg .,./auth,./certloader,./proxy,./wildcard,./socket

# Clean build output
clean:
Expand All @@ -31,11 +31,7 @@ test: unit $(INTEGRATION_TESTS)

# Run unit tests
unit:
go test -v -covermode=count -coverprofile=coverage-unit-test-base.out .
go test -v -covermode=count -coverprofile=coverage-unit-test-auth.out ./auth
go test -v -covermode=count -coverprofile=coverage-unit-test-certloader.out ./certloader
go test -v -covermode=count -coverprofile=coverage-unit-test-proxy.out ./proxy
go test -v -covermode=count -coverprofile=coverage-unit-test-wildcard.out ./wildcard
go test -v -covermode=count -coverprofile=coverage-unit-test.out ./...
.PHONY: unit

# Run integration tests
Expand Down
14 changes: 12 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -303,15 +303,25 @@ should work with any hardware security module that exposes a PKCS#11 interface.

See [HSM-PKCS11](docs/HSM-PKCS11.md) for details.

### PROXY protocol (experimental)
### Socket Activation (experimental)

Ghostunnel supports socket activation via both systemd (on Linux) and launchd
(on macOS). Socket activation is support for the `--listen` and `--status`
flags, and can be used by passing an address of the form `systemd:<name>` or
`launchd:<name>`, where `<name>` should be the name of the socket as defined in
your systemd/launchd configuration.

See [SOCKET-ACTIVATION](docs/SOCKET-ACTIVATION.md) for examples.

### PROXY Protocol (experimental)

Ghostunnel in server mode supports signalling of transport connection information
to the backend using the [PROXY protocol](https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt)
(v2), just pass the `--proxy-protocol` flag on startup. Note that the backend must
also support the PROXY protocol and must be configured to use it when setting
this option.

### macOS keychain support (experimental)
### MacOS Keychain Support (experimental)

If ghostunnel has been compiled with build tag `certstore` (off by default,
requires macOS 10.12+) a new flag will be available that allows for loading
Expand Down
70 changes: 70 additions & 0 deletions docs/SOCKET-ACTIVATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
Socket Activation
=================

Ghostunnel supports socket activation via both systemd (on Linux) and launchd
(on macOS). Socket activation is support for the `--listen` and `--status`
flags, and can be used by passing an address of the form `systemd:<name>` or
`launchd:<name>`, where `<name>` should be the name of the socket as defined in
your systemd/launchd configuration.

For example, a launchd plist to launch ghostunnel in server mode on :8081,
listening for status connections on :8082, and forwarding connections to :8083
could look like this:

```
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.square.ghostunnel</string>
<key>ProgramArguments</key>
<array>
<string>/usr/bin/ghostunnel</string>
<string>server</string>
<string>--keystore</string>
<string>/etc/ghostunnel/server-keystore.p12</string>
<string>--cacert</string>
<string>/etc/ghostunnel/cacert.pem</string>
<string>--target</string>
<string>localhost:8083</string>
<string>--listen</string>
<string>launchd:Listener</string>
<string>--status</string>
<string>launchd:Status</string>
<string>--allow-cn</string>
<string>client</string>
</array>
<key>StandardOutPath</key>
<string>/var/log/ghostunnel.out.log</string>
<key>StandardErrorPath</key>
<string>/var/log/ghostunnel.err.log</string>
<key>Sockets</key>
<dict>
<key>Listener</key>
<dict>
<key>SockServiceName</key>
<string>8081</string>
<key>SockType</key>
<string>stream</string>
<key>SockFamily</key>
<string>IPv4</string>
</dict>
<key>Status</key>
<dict>
<key>SockServiceName</key>
<string>8082</string>
<key>SockType</key>
<string>stream</string>
<key>SockFamily</key>
<string>IPv4</string>
</dict>
</dict>
</dict>
</plist>
```

Note that in the launchd case *both* `SockType` and `SockFamily` need to be
defined for each socket. If for example the family were to be left out, launchd
would open two sockets (IPv4 and IPv6) for the given key (like `Listener`) and
pass them to ghostunnel which is not currently supported.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/square/ghostunnel
require (
github.com/Masterminds/sprig v2.17.1+incompatible // indirect
github.com/aokoli/goutils v1.1.0 // indirect
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e
github.com/cyberdelia/go-metrics-graphite v0.0.0-20161219230853-39f87cc3b432
github.com/deathowl/go-metrics-prometheus v0.0.0-20181105123824-7cfe975c505b
github.com/google/uuid v1.1.0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ github.com/aokoli/goutils v1.1.0 h1:jy4ghdcYvs5EIoGssZNslIASX5m+KNMfyyKvRQ0TEVE=
github.com/aokoli/goutils v1.1.0/go.mod h1:SijmP0QR8LtwsmDs8Yii5Z/S4trXFGFC2oO5g9DP+DQ=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e h1:Wf6HqHfScWJN9/ZjdUKyjop4mf3Qdd+1TvvltAvM3m8=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/cyberdelia/go-metrics-graphite v0.0.0-20161219230853-39f87cc3b432 h1:M5QgkYacWj0Xs8MhpIK/5uwU02icXpEoSo9sM2aRCps=
github.com/cyberdelia/go-metrics-graphite v0.0.0-20161219230853-39f87cc3b432/go.mod h1:xwIwAxMvYnVrGJPe2FKx5prTrnAjGOD8zvDOnxnrrkM=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
Expand Down
12 changes: 0 additions & 12 deletions launchd_disabled.go

This file was deleted.

39 changes: 0 additions & 39 deletions launchd_enabled.go

This file was deleted.

101 changes: 25 additions & 76 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,13 @@ import (

graphite "github.com/cyberdelia/go-metrics-graphite"
gsyslog "github.com/hashicorp/go-syslog"
reuseport "github.com/kavu/go_reuseport"
http_dialer "github.com/mwitkow/go-http-dialer"
"github.com/prometheus/client_golang/prometheus/promhttp"
metrics "github.com/rcrowley/go-metrics"
"github.com/square/ghostunnel/auth"
"github.com/square/ghostunnel/certloader"
"github.com/square/ghostunnel/proxy"
"github.com/square/ghostunnel/socket"
"github.com/square/ghostunnel/wildcard"
sqmetrics "github.com/square/go-sq-metrics"
kingpin "gopkg.in/alecthomas/kingpin.v2"
Expand All @@ -65,7 +65,7 @@ var (
app = kingpin.New("ghostunnel", "A simple SSL/TLS proxy with mutual authentication for securing non-TLS services.")

serverCommand = app.Command("server", "Server mode (TLS listener -> plain TCP/UNIX target).")
serverListenAddress = serverCommand.Flag("listen", "Address and port to listen on (HOST:PORT).").PlaceHolder("ADDR").Required().TCP()
serverListenAddress = serverCommand.Flag("listen", "Address and port to listen on (HOST:PORT, or unix:PATH).").PlaceHolder("ADDR").Required().String()
serverForwardAddress = serverCommand.Flag("target", "Address to forward connections to (HOST:PORT, or unix:PATH).").PlaceHolder("ADDR").Required().String()
serverProxyProtocol = serverCommand.Flag("proxy-protocol", "Enable PROXY protocol v2 to signal connection info to backend").Bool()
serverUnsafeTarget = serverCommand.Flag("unsafe-target", "If set, does not limit target to localhost, 127.0.0.1, [::1], or UNIX sockets.").Bool()
Expand Down Expand Up @@ -203,22 +203,20 @@ func validateFlags(app *kingpin.Application) error {
return nil
}

// Validates that addr is either a unix socket or localhost
func validateUnixOrLocalhost(addr string) bool {
if strings.HasPrefix(addr, "unix:") {
return true
}
if strings.HasPrefix(addr, "127.0.0.1:") {
return true
}
if strings.HasPrefix(addr, "[::1]:") {
return true
}
if strings.HasPrefix(addr, "localhost:") {
return true
}
if addr == "launchd" {
return true
// Validates that addr is "safe" and does not need --unsafe-listen (or --unsafe-target).
func consideredSafe(addr string) bool {
safePrefixes := []string{
"unix:",
"systemd:",
"launchd:",
"127.0.0.1:",
"[::1]:",
"localhost:",
}
for _, prefix := range safePrefixes {
if strings.HasPrefix(addr, prefix) {
return true
}
}
return false
}
Expand Down Expand Up @@ -256,7 +254,7 @@ func serverValidateFlags() error {
if *serverDisableAuth && (*serverAllowAll || hasAccessFlags) {
return errors.New("--disable-authentication is mutually exclusive with other access control flags")
}
if !*serverUnsafeTarget && !validateUnixOrLocalhost(*serverForwardAddress) {
if !*serverUnsafeTarget && !consideredSafe(*serverForwardAddress) {
return errors.New("--target must be unix:PATH, localhost:PORT, 127.0.0.1:PORT or [::1]:PORT (unless --unsafe-target is set)")
}

Expand All @@ -279,7 +277,7 @@ func clientValidateFlags() error {
(hasKeychainIdentity() && *clientDisableAuth) {
return errors.New("--keystore, --keychain-identity, and --disable-authentication flags are mutually exclusive")
}
if !*clientUnsafeListen && !validateUnixOrLocalhost(*clientListenAddress) {
if !*clientUnsafeListen && !consideredSafe(*clientListenAddress) {
return fmt.Errorf("--listen must be unix:PATH, localhost:PORT, 127.0.0.1:PORT or [::1]:PORT (unless --unsafe-listen is set)")
}
if *clientConnectProxy != nil && (*clientConnectProxy).Scheme != "http" && (*clientConnectProxy).Scheme != "https" {
Expand Down Expand Up @@ -388,7 +386,7 @@ func run(args []string) error {
return err
}

network, address, host, err := parseUnixOrTCPAddress(*clientForwardAddress)
network, address, host, err := socket.ParseAddress(*clientForwardAddress)
if err != nil {
fmt.Fprintf(os.Stderr, "error: invalid target address: %s\n", err)
return err
Expand Down Expand Up @@ -451,7 +449,7 @@ func serverListen(context *Context) error {
config.VerifyPeerCertificate = serverACL.VerifyPeerCertificateServer
}

listener, err := reuseport.NewReusablePortListener("tcp", (*serverListenAddress).String())
listener, err := socket.ParseAndOpen(*serverListenAddress)
if err != nil {
logger.Printf("error trying to listen: %s", err)
return err
Expand All @@ -474,7 +472,7 @@ func serverListen(context *Context) error {
}
}

logger.Printf("listening for connections on %s", (*serverListenAddress).String())
logger.Printf("listening for connections on %s", *serverListenAddress)

go p.Accept()

Expand All @@ -487,19 +485,7 @@ func serverListen(context *Context) error {

// Open listening socket in client mode.
func clientListen(context *Context) error {
// Setup listening socket
network, address, _, err := parseUnixOrTCPAddress(*clientListenAddress)
if err != nil {
logger.Printf("error parsing client listen address: %s", err)
return err
}

var listener net.Listener
if network == "launchd" {
listener, err = LaunchdSocket()
} else {
listener, err = net.Listen(network, address)
}
listener, err := socket.ParseAndOpen(*clientListenAddress)
if err != nil {
logger.Printf("error opening socket: %s", err)
return err
Expand Down Expand Up @@ -571,19 +557,12 @@ func (context *Context) serveStatus() error {
config.GetCertificate = context.cert.GetCertificate
}

network, address, _, err := parseUnixOrTCPAddress(*statusAddress)
network, address, _, err := socket.ParseAddress(*statusAddress)
if err != nil {
return err
}

var listener net.Listener
if network == "unix" {
listener, err = net.Listen(network, address)
listener.(*net.UnixListener).SetUnlinkOnClose(true)
} else {
listener, err = reuseport.NewReusablePortListener(network, address)
}

listener, err := socket.Open(network, address)
if err != nil {
fmt.Fprintf(os.Stderr, "error: unable to bind on status port: %s\n", err)
return err
Expand All @@ -610,7 +589,7 @@ func (context *Context) serveStatus() error {

// Get backend dialer function in server mode (connecting to a unix socket or tcp port)
func serverBackendDialer() (func() (net.Conn, error), error) {
backendNet, backendAddr, _, err := parseUnixOrTCPAddress(*serverForwardAddress)
backendNet, backendAddr, _, err := socket.ParseAddress(*serverForwardAddress)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -672,36 +651,6 @@ func clientBackendDialer(cert certloader.Certificate, network, address, host str
return func() (net.Conn, error) { return d.Dial(network, address) }, nil
}

// Parse a string representing a TCP address or UNIX socket for our backend
// target. The input can be or the form "HOST:PORT" for TCP or "unix:PATH"
// for a UNIX socket.
func parseUnixOrTCPAddress(input string) (network, address, host string, err error) {
if strings.HasPrefix(input, "launchd") {
network = "launchd"
return
}

if strings.HasPrefix(input, "unix:") {
network = "unix"
address = input[5:]
return
}

host, _, err = net.SplitHostPort(input)
if err != nil {
return
}

// Make sure target address resolves
_, err = net.ResolveTCPAddr("tcp", input)
if err != nil {
return
}

network, address = "tcp", input
return
}

func proxyLoggerFlags(flags []string) int {
out := proxy.LogEverything
for _, flag := range flags {
Expand Down
Loading

0 comments on commit e599171

Please sign in to comment.