Skip to content

Commit

Permalink
Implementation of PROXY command for HAProxy
Browse files Browse the repository at this point in the history
Can be enabled with "proxyon" : true," in the server config
  • Loading branch information
phires committed Jun 6, 2024
1 parent 13718a2 commit dba43a9
Show file tree
Hide file tree
Showing 7 changed files with 79 additions and 1 deletion.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
.idea
guerrillad
goguerrilla.conf
goguerrilla.conf.json
dist
Expand Down
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ help:
@echo " test to run unittests"

clean:
rm -f guerrillad
rm -rf dist/*

vendor:
Expand All @@ -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
Expand Down
2 changes: 2 additions & 0 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
15 changes: 15 additions & 0 deletions server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down
55 changes: 55 additions & 0 deletions server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit dba43a9

Please sign in to comment.