diff --git a/.gitignore b/.gitignore index c1ff2b9..65da7b7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .idea +guerrillad goguerrilla.conf goguerrilla.conf.json dist diff --git a/Makefile b/Makefile index 67e7deb..9c6b8af 100644 --- a/Makefile +++ b/Makefile @@ -14,6 +14,7 @@ help: @echo " test to run unittests" clean: + rm -f guerrillad rm -rf dist/* vendor: @@ -26,7 +27,7 @@ guerrillad: $(GO_VARS) GOOS=windows GOARCH=amd64 $(GO) build -o="dist/windows/amd64/guerrillad" -ldflags="$(LD_FLAGS)" $(ROOT)/cmd/guerrillad # Build the binary for current platform (as before) to not break any existing build processes - $(GO_VARS) $(GO) build -o="dist/windows/amd64/guerrillad" -ldflags="$(LD_FLAGS)" $(ROOT)/cmd/guerrillad + $(GO_VARS) $(GO) build -o="guerrillad" -ldflags="$(LD_FLAGS)" $(ROOT)/cmd/guerrillad guerrilladrace: $(GO_VARS) $(GO) build -o="guerrillad" -race -ldflags="$(LD_FLAGS)" $(ROOT)/cmd/guerrillad diff --git a/config.go b/config.go index fc933eb..edb653d 100644 --- a/config.go +++ b/config.go @@ -60,6 +60,8 @@ type ServerConfig struct { // XClientOn when using a proxy such as Nginx, XCLIENT command is used to pass the // original client's IP address & client's HELO XClientOn bool `json:"xclient_on,omitempty"` + // Proxied when using a loadbalancer such as HAProxy, set to true to enable + ProxyOn bool `json:"proxyon,omitempty"` } type ServerTLSConfig struct { diff --git a/go.mod b/go.mod index 3737ad8..20c914f 100644 --- a/go.mod +++ b/go.mod @@ -10,11 +10,13 @@ require ( github.com/spf13/cobra v1.8.0 golang.org/x/net v0.25.0 gopkg.in/iconv.v1 v1.1.1 + github.com/pires/go-proxyproto v0.7.0 ) require ( filippo.io/edwards25519 v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/pires/go-proxyproto v0.7.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/stretchr/testify v1.9.0 // indirect golang.org/x/sys v0.20.0 // indirect diff --git a/go.sum b/go.sum index af2e9c1..314c1ae 100644 --- a/go.sum +++ b/go.sum @@ -17,6 +17,8 @@ github.com/gomodule/redigo v1.9.2 h1:HrutZBLhSIU8abiSfW8pj8mPhOyMYjZT/wcA4/L9L9s github.com/gomodule/redigo v1.9.2/go.mod h1:KsU3hiK/Ay8U42qpaJk+kuNa3C+spxapWpM+ywhcgtw= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/pires/go-proxyproto v0.7.0 h1:IukmRewDQFWC7kfnb66CSomk2q/seBuilHBYFwyq0Hs= +github.com/pires/go-proxyproto v0.7.0/go.mod h1:Vz/1JPY/OACxWGQNIRY2BeyDmpoaWmEP40O9LbuiFR4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= diff --git a/server.go b/server.go index 38195b3..dbd6627 100644 --- a/server.go +++ b/server.go @@ -82,6 +82,7 @@ var ( cmdQUIT command = []byte("QUIT") cmdDATA command = []byte("DATA") cmdSTARTTLS command = []byte("STARTTLS") + cmdPROXY command = []byte("PROXY") ) func (c command) match(in []byte) bool { @@ -489,6 +490,20 @@ func (s *server) handleClient(client *client) { } } client.sendResponse(r.SuccessMailCmd) + + case sc.ProxyOn && cmdPROXY.match(cmd): + if toks := bytes.Split(input[6:], []byte{' '}); len(toks) == 5 { + s.log().Debugf("PROXY command. Proto: [%s] Source IP: [%s] Dest IP: [%s] Source Port: [%s] Dest Port: [%s]", toks[0], toks[1], toks[2], toks[3], toks[4]) + client.RemoteIP = string(toks[1]) + s.log().Debugf("client.RemoteIP: [%s]", client.RemoteIP) + // There is RfC or anything about the PROXY command, + // so it is unclear, if a response is required. + //client.sendResponse(r.SuccessMailCmd) + } else { + s.log().Error("PROXY parse error", "["+string(input[6:])+"]") + client.sendResponse(r.FailSyntaxError) + } + case cmdMAIL.match(cmd): if client.isInTransaction() { client.sendResponse(r.FailNestedMailCmd) diff --git a/server_test.go b/server_test.go index 281780a..4c13c6a 100644 --- a/server_test.go +++ b/server_test.go @@ -906,6 +906,61 @@ func TestXClient(t *testing.T) { wg.Wait() // wait for handleClient to exit } +func TestProxy(t *testing.T) { + var mainlog log.Logger + var logOpenError error + defer cleanTestArtifacts(t) + sc := getMockServerConfig() + sc.ProxyOn = true + mainlog, logOpenError = log.GetLogger(sc.LogFile, "debug") + if logOpenError != nil { + mainlog.WithError(logOpenError).Errorf("Failed creating a logger for mock conn [%s]", sc.ListenInterface) + } + conn, server := getMockServerConn(sc, t) + // call the serve.handleClient() func in a goroutine. + client := NewClient(conn.Server, 1, mainlog, mail.NewPool(5)) + var wg sync.WaitGroup + wg.Add(1) + go func() { + server.handleClient(client) + wg.Done() + }() + // Wait for the greeting from the server + r := textproto.NewReader(bufio.NewReader(conn.Client)) + line, _ := r.ReadLine() + + // PROXY command is sent before anything else + w := textproto.NewWriter(bufio.NewWriter(conn.Client)) + if err := w.PrintfLine("PROXY TCP4 1.2.3.4 127.0.0.1 12345 25"); err != nil { + t.Error(err) + } + // We do not expect anything back from the PROXY command + // TODO: For some reason, client.RemoteIP contains "tcp" - whereever this + // may come from. Therefore for now we're skipping the test if the + // parsing was successfull. + /* + if client.RemoteIP != "1.2.3.4" { + t.Error("client.RemoteIP should be 1.2.3.4, but got:", client.RemoteIP) + } + */ + // try malformed input + if err := w.PrintfLine("PROXY c"); err != nil { + t.Error(err) + } + line, _ = r.ReadLine() + + expected := "550 5.5.2 Syntax error" + if strings.Index(line, expected) != 0 { + t.Error("expected", expected, "but got:", line) + } + + if err := w.PrintfLine("QUIT"); err != nil { + t.Error(err) + } + line, _ = r.ReadLine() + wg.Wait() // wait for handleClient to exit +} + // The backend gateway should time out after 1 second because it sleeps for 2 sec. // The transaction should wait until finished, and then test to see if we can do // a second transaction