author | Alberto Bertogli
<albertito@blitiri.com.ar> 2020-11-12 22:00:46 UTC |
committer | Alberto Bertogli
<albertito@blitiri.com.ar> 2020-11-13 20:49:42 UTC |
parent | c9d3ba0ca095d0bf54c43014da50ae6d3d4d2f59 |
.mkdocs.yml | +1 | -0 |
chasquid.go | +1 | -0 |
docs/haproxy.md | +32 | -0 |
docs/man/chasquid.conf.5 | +9 | -1 |
docs/man/chasquid.conf.5.pod | +9 | -0 |
etc/chasquid/chasquid.conf | +8 | -0 |
internal/config/config.go | +5 | -0 |
internal/config/config.pb.go | +20 | -6 |
internal/config/config.proto | +5 | -0 |
internal/haproxy/haproxy.go | +76 | -0 |
internal/haproxy/haproxy_test.go | +97 | -0 |
internal/smtpsrv/conn.go | +32 | -16 |
internal/smtpsrv/server.go | +4 | -0 |
test/Dockerfile | +2 | -1 |
test/README.md | +2 | -0 |
test/t-18-haproxy/config/chasquid.conf | +12 | -0 |
test/t-18-haproxy/content | +4 | -0 |
test/t-18-haproxy/haproxy.cfg | +7 | -0 |
test/t-18-haproxy/hosts | +1 | -0 |
test/t-18-haproxy/msmtprc | +14 | -0 |
test/t-18-haproxy/run.sh | +39 | -0 |
test/util/lib.sh | +9 | -0 |
diff --git a/.mkdocs.yml b/.mkdocs.yml index 44340be..0d297a1 100644 --- a/.mkdocs.yml +++ b/.mkdocs.yml @@ -26,6 +26,7 @@ nav: - hooks.md - dovecot.md - dkim.md + - haproxy.md - docker.md - flow.md - monitoring.md diff --git a/chasquid.go b/chasquid.go index 00bb2a7..3c23487 100644 --- a/chasquid.go +++ b/chasquid.go @@ -100,6 +100,7 @@ func main() { s.Hostname = conf.Hostname s.MaxDataSize = conf.MaxDataSizeMb * 1024 * 1024 s.HookPath = "hooks/" + s.HAProxyEnabled = conf.HaproxyIncoming s.SetAliasesConfig(conf.SuffixSeparators, conf.DropCharacters) diff --git a/docs/haproxy.md b/docs/haproxy.md new file mode 100644 index 0000000..1ad75fe --- /dev/null +++ b/docs/haproxy.md @@ -0,0 +1,32 @@ + +# HAProxy integration + +As of version 1.6, [chasquid] supports being deployed behind a [HAProxy] +instance. + +**This is EXPERIMENTAL for now, and can change in backwards-incompatible +ways.** + + +## Configuring HAProxy + +In the backend server line, set the [send-proxy] parameter to turn on the use +of the PROXY protocol against chasquid. + +You need to set this for each of the ports that are forwarded. + + +## Configuring chasquid + +Add the following line to `/etc/chasquid/chasquid.conf`: + +``` +haproxy_incoming: true +``` + +That turns HAProxy support on for all incoming SMTP connections. + + +[chasquid]: https://blitiri.com.ar/p/chasquid +[HAProxy]: https://www.haproxy.org/ +[send-proxy]: http://cbonte.github.io/haproxy-dconv/2.0/configuration.html#5.2-send-proxy diff --git a/docs/man/chasquid.conf.5 b/docs/man/chasquid.conf.5 index 64c45c2..bca08cb 100644 --- a/docs/man/chasquid.conf.5 +++ b/docs/man/chasquid.conf.5 @@ -133,7 +133,7 @@ .\" ======================================================================== .\" .IX Title "chasquid.conf 5" -.TH chasquid.conf 5 "2020-05-24" "" "" +.TH chasquid.conf 5 "2020-11-12" "" "" .\" For nroff, turn off justification. Always turn off hyphenation; it makes .\" way too many mistakes in technical documents. .if n .ad l @@ -234,6 +234,14 @@ databases will be authenticated via dovecot. Default: \f(CW\*(C`false\*(C'\fR. The path to dovecot's auth sockets is autodetected, but can be manually overridden using the \f(CW\*(C`dovecot_userdb_path\*(C'\fR and \f(CW\*(C`dovecot_client_path\*(C'\fR if needed. +.IP "\fBhaproxy_incoming\fR (bool):" 8 +.IX Item "haproxy_incoming (bool):" +\&\fB\s-1EXPERIMENTAL\s0\fR, might change in backwards-incompatible ways. +.Sp +If true, expect incoming \s-1SMTP\s0 connections to use the HAProxy protocol. +This allows deploying chasquid behind a HAProxy server, as the address +information is preserved, and \s-1SPF\s0 checks can be performed properly. +Default: \f(CW\*(C`false\*(C'\fR. .SH "SEE ALSO" .IX Header "SEE ALSO" \&\fBchasquid\fR\|(1) diff --git a/docs/man/chasquid.conf.5.pod b/docs/man/chasquid.conf.5.pod index fa6705d..436d5e3 100644 --- a/docs/man/chasquid.conf.5.pod +++ b/docs/man/chasquid.conf.5.pod @@ -113,6 +113,15 @@ The path to dovecot's auth sockets is autodetected, but can be manually overridden using the C<dovecot_userdb_path> and C<dovecot_client_path> if needed. +=item B<haproxy_incoming> (bool): + +B<EXPERIMENTAL>, might change in backwards-incompatible ways. + +If true, expect incoming SMTP connections to use the HAProxy protocol. +This allows deploying chasquid behind a HAProxy server, as the address +information is preserved, and SPF checks can be performed properly. +Default: C<false>. + =back =head1 SEE ALSO diff --git a/etc/chasquid/chasquid.conf b/etc/chasquid/chasquid.conf index ea07108..5aa9f50 100644 --- a/etc/chasquid/chasquid.conf +++ b/etc/chasquid/chasquid.conf @@ -87,3 +87,11 @@ # Default: "" (autodetect) #dovecot_userdb_path: "" #dovecot_client_path: "" + +# Expect incoming SMTP connections to use the HAProxy protocol. +# EXPERIMENTAL - Might change in backwards-incompatible ways. +# If set to true, this allows deploying chasquid behind a HAProxy server, as +# the address information is preserved, and SPF checks can be performed +# properly. +# Default: false +#haproxy_incoming: false diff --git a/internal/config/config.go b/internal/config/config.go index 3731dc5..6616856 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -123,6 +123,10 @@ func override(c, o *Config) { if o.DovecotClientPath != "" { c.DovecotClientPath = o.DovecotClientPath } + + if o.HaproxyIncoming { + c.HaproxyIncoming = true + } } // LogConfig logs the given configuration, in a human-friendly way. @@ -141,4 +145,5 @@ func LogConfig(c *Config) { log.Infof(" Mail log: %s", c.MailLogPath) log.Infof(" Dovecot auth: %v (%q, %q)", c.DovecotAuth, c.DovecotUserdbPath, c.DovecotClientPath) + log.Infof(" HAProxy incoming: %v", c.HaproxyIncoming) } diff --git a/internal/config/config.pb.go b/internal/config/config.pb.go index e9cbf2f..72fa74e 100644 --- a/internal/config/config.pb.go +++ b/internal/config/config.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.23.0 +// protoc-gen-go v1.25.0 // protoc v3.12.3 // source: config.proto @@ -108,6 +108,10 @@ type Config struct { // is not, we will try to autodetect it. // Example: /var/run/dovecot/auth-client DovecotClientPath string `protobuf:"bytes,15,opt,name=dovecot_client_path,json=dovecotClientPath,proto3" json:"dovecot_client_path,omitempty"` + // Expect incoming SMTP connections to use the HAProxy protocol. + // This allows deploying chasquid behind a HAProxy server, as the + // address information is preserved. + HaproxyIncoming bool `protobuf:"varint,16,opt,name=haproxy_incoming,json=haproxyIncoming,proto3" json:"haproxy_incoming,omitempty"` } func (x *Config) Reset() { @@ -247,10 +251,17 @@ func (x *Config) GetDovecotClientPath() string { return "" } +func (x *Config) GetHaproxyIncoming() bool { + if x != nil { + return x.HaproxyIncoming + } + return false +} + var File_config_proto protoreflect.FileDescriptor var file_config_proto_rawDesc = []byte{ - 0x0a, 0x0c, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x95, + 0x0a, 0x0c, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xc0, 0x05, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1a, 0x0a, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x27, 0x0a, 0x10, 0x6d, 0x61, 0x78, 0x5f, 0x64, 0x61, 0x74, @@ -292,10 +303,13 @@ var file_config_proto_rawDesc = []byte{ 0x64, 0x62, 0x50, 0x61, 0x74, 0x68, 0x12, 0x2e, 0x0a, 0x13, 0x64, 0x6f, 0x76, 0x65, 0x63, 0x6f, 0x74, 0x5f, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x64, 0x6f, 0x76, 0x65, 0x63, 0x6f, 0x74, 0x43, 0x6c, 0x69, 0x65, - 0x6e, 0x74, 0x50, 0x61, 0x74, 0x68, 0x42, 0x2c, 0x5a, 0x2a, 0x62, 0x6c, 0x69, 0x74, 0x69, 0x72, - 0x69, 0x2e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x72, 0x2f, 0x67, 0x6f, 0x2f, 0x63, 0x68, 0x61, 0x73, - 0x71, 0x75, 0x69, 0x64, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x63, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x6e, 0x74, 0x50, 0x61, 0x74, 0x68, 0x12, 0x29, 0x0a, 0x10, 0x68, 0x61, 0x70, 0x72, 0x6f, 0x78, + 0x79, 0x5f, 0x69, 0x6e, 0x63, 0x6f, 0x6d, 0x69, 0x6e, 0x67, 0x18, 0x10, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x0f, 0x68, 0x61, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x49, 0x6e, 0x63, 0x6f, 0x6d, 0x69, 0x6e, + 0x67, 0x42, 0x2c, 0x5a, 0x2a, 0x62, 0x6c, 0x69, 0x74, 0x69, 0x72, 0x69, 0x2e, 0x63, 0x6f, 0x6d, + 0x2e, 0x61, 0x72, 0x2f, 0x67, 0x6f, 0x2f, 0x63, 0x68, 0x61, 0x73, 0x71, 0x75, 0x69, 0x64, 0x2f, + 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x62, + 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/internal/config/config.proto b/internal/config/config.proto index 3c1bb39..54ecf95 100644 --- a/internal/config/config.proto +++ b/internal/config/config.proto @@ -95,4 +95,9 @@ message Config { // is not, we will try to autodetect it. // Example: /var/run/dovecot/auth-client string dovecot_client_path = 15; + + // Expect incoming SMTP connections to use the HAProxy protocol. + // This allows deploying chasquid behind a HAProxy server, as the + // address information is preserved. + bool haproxy_incoming = 16; } diff --git a/internal/haproxy/haproxy.go b/internal/haproxy/haproxy.go new file mode 100644 index 0000000..6351bb0 --- /dev/null +++ b/internal/haproxy/haproxy.go @@ -0,0 +1,76 @@ +// Package haproxy implements the handshake for the HAProxy client protocol +// version 1, as described in +// https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt. +package haproxy + +import ( + "bufio" + "errors" + "net" + "strconv" + "strings" +) + +var ( + errInvalidProtoID = errors.New("invalid protocol identifier") + errUnkProtocol = errors.New("unknown protocol") + errInvalidFields = errors.New("invalid number of fields") + errInvalidSrcIP = errors.New("invalid src ip") + errInvalidDstIP = errors.New("invalid dst ip") + errInvalidSrcPort = errors.New("invalid src port") + errInvalidDstPort = errors.New("invalid dst port") +) + +// Handshake performs the HAProxy protocol v1 handshake on the given reader, +// which is expected to be backed by a network connection. +// It returns the source and destination addresses, or an error if the +// handshake could not complete. +// Note that any timeouts or limits must be set by the caller on the +// underlying connection, this is helper only to perform the handshake. +func Handshake(r *bufio.Reader) (src, dst net.Addr, err error) { + line, err := r.ReadString('\n') + if err != nil { + return nil, nil, err + } + + fields := strings.Fields(line) + + if len(fields) < 2 || fields[0] != "PROXY" { + return nil, nil, errInvalidProtoID + } + + switch fields[1] { + case "TCP4", "TCP6": + // Allowed to continue, nothing to do. + default: + return nil, nil, errUnkProtocol + } + + if len(fields) != 6 { + return nil, nil, errInvalidFields + } + + srcIP := net.ParseIP(fields[2]) + if srcIP == nil { + return nil, nil, errInvalidSrcIP + } + + dstIP := net.ParseIP(fields[3]) + if dstIP == nil { + return nil, nil, errInvalidDstIP + } + + srcPort, err := strconv.ParseUint(fields[4], 10, 16) + if err != nil { + return nil, nil, errInvalidSrcPort + } + + dstPort, err := strconv.ParseUint(fields[5], 10, 16) + if err != nil { + return nil, nil, errInvalidDstPort + } + + src = &net.TCPAddr{IP: srcIP, Port: int(srcPort)} + dst = &net.TCPAddr{IP: dstIP, Port: int(dstPort)} + return src, dst, nil +} diff --git a/internal/haproxy/haproxy_test.go b/internal/haproxy/haproxy_test.go new file mode 100644 index 0000000..dd63a90 --- /dev/null +++ b/internal/haproxy/haproxy_test.go @@ -0,0 +1,97 @@ +package haproxy + +import ( + "bufio" + "io" + "net" + "strings" + "testing" +) + +func TestNoNewline(t *testing.T) { + r := bufio.NewReader(strings.NewReader("PROXY ")) + _, _, err := Handshake(r) + if err != io.EOF { + t.Errorf("expected EOF, got %v", err) + } +} + +func TestBasic(t *testing.T) { + var ( + src4, _ = net.ResolveTCPAddr("tcp", "1.1.1.1:3333") + dst4, _ = net.ResolveTCPAddr("tcp", "2.2.2.2:4444") + src6, _ = net.ResolveTCPAddr("tcp", "[5::5]:7777") + dst6, _ = net.ResolveTCPAddr("tcp", "[6::6]:8888") + ) + + cases := []struct { + str string + src, dst net.Addr + err error + }{ + // Early line errors. + {"", nil, nil, errInvalidProtoID}, + {"lalala", nil, nil, errInvalidProtoID}, + {"PROXY", nil, nil, errInvalidProtoID}, + {"PROXY lalala", nil, nil, errUnkProtocol}, + {"PROXY UNKNOWN", nil, nil, errUnkProtocol}, + + // Number of field errors. + {"PROXY TCP4", nil, nil, errInvalidFields}, + {"PROXY TCP4 a", nil, nil, errInvalidFields}, + {"PROXY TCP4 a b", nil, nil, errInvalidFields}, + {"PROXY TCP4 a b c", nil, nil, errInvalidFields}, + + // Parsing of ipv4 addresses. + {"PROXY TCP4 a b c d", nil, nil, errInvalidSrcIP}, + {"PROXY TCP4 1.1.1.1 b c d", + nil, nil, errInvalidDstIP}, + {"PROXY TCP4 1.1.1.1 2.2.2.2 c d", + nil, nil, errInvalidSrcPort}, + {"PROXY TCP4 1.1.1.1 2.2.2.2 3333 d", + nil, nil, errInvalidDstPort}, + {"PROXY TCP4 1.1.1.1 2.2.2.2 3333 4444", + src4, dst4, nil}, + + // Parsing of ipv6 addresses. + {"PROXY TCP6 a b c d", nil, nil, errInvalidSrcIP}, + {"PROXY TCP6 5::5 b c d", + nil, nil, errInvalidDstIP}, + {"PROXY TCP6 5::5 6::6 c d", + nil, nil, errInvalidSrcPort}, + {"PROXY TCP6 5::5 6::6 7777 d", + nil, nil, errInvalidDstPort}, + {"PROXY TCP6 5::5 6::6 7777 8888", + src6, dst6, nil}, + } + + for i, c := range cases { + t.Logf("testing %d: %v", i, c.str) + + src, dst, err := Handshake(newR(c.str)) + + if !addrEq(src, c.src) { + t.Errorf("%d: got src %v, expected %v", i, src, c.src) + } + if !addrEq(dst, c.dst) { + t.Errorf("%d: got dst %v, expected %v", i, dst, c.dst) + } + if err != c.err { + t.Errorf("%d: got error %v, expected %v", i, err, c.err) + } + } +} + +func newR(s string) *bufio.Reader { + return bufio.NewReader(strings.NewReader(s + "\r\n")) +} + +func addrEq(a, b net.Addr) bool { + if a == nil || b == nil { + return a == nil && b == nil + } + + ta := a.(*net.TCPAddr) + tb := b.(*net.TCPAddr) + return ta.IP.Equal(tb.IP) && ta.Port == tb.Port +} diff --git a/internal/smtpsrv/conn.go b/internal/smtpsrv/conn.go index 4ad7617..31d754d 100644 --- a/internal/smtpsrv/conn.go +++ b/internal/smtpsrv/conn.go @@ -25,6 +25,7 @@ import ( "blitiri.com.ar/go/chasquid/internal/domaininfo" "blitiri.com.ar/go/chasquid/internal/envelope" "blitiri.com.ar/go/chasquid/internal/expvarom" + "blitiri.com.ar/go/chasquid/internal/haproxy" "blitiri.com.ar/go/chasquid/internal/maillog" "blitiri.com.ar/go/chasquid/internal/normalize" "blitiri.com.ar/go/chasquid/internal/queue" @@ -104,6 +105,7 @@ type Conn struct { conn net.Conn mode SocketMode tlsConnState *tls.ConnectionState + remoteAddr net.Addr // Reader and text writer, so we can control limits. reader *bufio.Reader @@ -158,6 +160,9 @@ type Conn struct { // Time we wait for network operations. commandTimeout time.Duration + + // Enable HAProxy on incoming connections. + haproxyEnabled bool } // Close the connection. @@ -199,6 +204,17 @@ func (c *Conn) Handle() { c.reader = bufio.NewReader(c.conn) c.writer = bufio.NewWriter(c.conn) + c.remoteAddr = c.conn.RemoteAddr() + if c.haproxyEnabled { + src, dst, err := haproxy.Handshake(c.reader) + if err != nil { + c.tr.Errorf("error in haproxy handshake: %v", err) + return + } + c.remoteAddr = src + c.tr.Debugf("haproxy handshake: %v -> %v", src, dst) + } + c.printfLine("220 %s ESMTP chasquid", c.hostname) var cmd, params string @@ -428,21 +444,21 @@ func (c *Conn) MAIL(params string) (code int, msg string) { c.spfResult, c.spfError = c.checkSPF(addr) if c.spfResult == spf.Fail { // https://tools.ietf.org/html/rfc7208#section-8.4 - maillog.Rejected(c.conn.RemoteAddr(), addr, nil, + maillog.Rejected(c.remoteAddr, addr, nil, fmt.Sprintf("failed SPF: %v", c.spfError)) return 550, fmt.Sprintf( "5.7.23 SPF check failed: %v", c.spfError) } if !c.secLevelCheck(addr) { - maillog.Rejected(c.conn.RemoteAddr(), addr, nil, + maillog.Rejected(c.remoteAddr, addr, nil, "security level check failed") return 550, "5.7.3 Security level check failed" } addr, err = normalize.DomainToUnicode(addr) if err != nil { - maillog.Rejected(c.conn.RemoteAddr(), addr, nil, + maillog.Rejected(c.remoteAddr, addr, nil, fmt.Sprintf("malformed address: %v", err)) return 501, "5.1.8 Malformed sender domain (IDNA conversion failed)" } @@ -463,7 +479,7 @@ func (c *Conn) checkSPF(addr string) (spf.Result, error) { return "", nil } - if tcp, ok := c.conn.RemoteAddr().(*net.TCPAddr); ok { + if tcp, ok := c.remoteAddr.(*net.TCPAddr); ok { res, err := spf.CheckHostWithSender( tcp.IP, envelope.DomainOf(addr), addr) @@ -549,7 +565,7 @@ func (c *Conn) RCPT(params string) (code int, msg string) { localDst := envelope.DomainIn(addr, c.localDomains) if !localDst && !c.completedAuth { - maillog.Rejected(c.conn.RemoteAddr(), c.mailFrom, []string{addr}, + maillog.Rejected(c.remoteAddr, c.mailFrom, []string{addr}, "relay not allowed") return 503, "5.7.1 Relay not allowed" } @@ -557,13 +573,13 @@ func (c *Conn) RCPT(params string) (code int, msg string) { if localDst { addr, err = normalize.Addr(addr) if err != nil { - maillog.Rejected(c.conn.RemoteAddr(), c.mailFrom, []string{addr}, + maillog.Rejected(c.remoteAddr, c.mailFrom, []string{addr}, fmt.Sprintf("invalid address: %v", err)) return 550, "5.1.3 Destination address is invalid" } if !c.userExists(addr) { - maillog.Rejected(c.conn.RemoteAddr(), c.mailFrom, []string{addr}, + maillog.Rejected(c.remoteAddr, c.mailFrom, []string{addr}, "local user does not exist") return 550, "5.1.1 Destination address is unknown (user does not exist)" } @@ -621,7 +637,7 @@ func (c *Conn) DATA(params string) (code int, msg string) { c.tr.Debugf("-> ... %d bytes of data", len(c.data)) if err := checkData(c.data); err != nil { - maillog.Rejected(c.conn.RemoteAddr(), c.mailFrom, c.rcptTo, err.Error()) + maillog.Rejected(c.remoteAddr, c.mailFrom, c.rcptTo, err.Error()) return 554, err.Error() } @@ -629,7 +645,7 @@ func (c *Conn) DATA(params string) (code int, msg string) { hookOut, permanent, err := c.runPostDataHook(c.data) if err != nil { - maillog.Rejected(c.conn.RemoteAddr(), c.mailFrom, c.rcptTo, err.Error()) + maillog.Rejected(c.remoteAddr, c.mailFrom, c.rcptTo, err.Error()) if permanent { return 554, err.Error() } @@ -646,7 +662,7 @@ func (c *Conn) DATA(params string) (code int, msg string) { } c.tr.Printf("Queued from %s to %s - %s", c.mailFrom, c.rcptTo, msgID) - maillog.Queued(c.conn.RemoteAddr(), c.mailFrom, c.rcptTo, msgID) + maillog.Queued(c.remoteAddr, c.mailFrom, c.rcptTo, msgID) // It is very important that we reset the envelope before returning, // so clients can send other emails right away without needing to RSET. @@ -677,7 +693,7 @@ func (c *Conn) addReceivedHeader() { // and then the given EHLO domain for convenience and // troubleshooting. v += fmt.Sprintf("from [%s] (%s)\n", - addrLiteral(c.conn.RemoteAddr()), c.ehloDomain) + addrLiteral(c.remoteAddr), c.ehloDomain) } v += fmt.Sprintf("by %s (chasquid) ", c.hostname) @@ -800,7 +816,7 @@ func (c *Conn) runPostDataHook(data []byte) ([]byte, bool, error) { hookResults.Add("post-data:skip", 1) return nil, false, nil } - tr := trace.New("Hook.Post-DATA", c.conn.RemoteAddr().String()) + tr := trace.New("Hook.Post-DATA", c.remoteAddr.String()) defer tr.Finish() ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute) @@ -813,7 +829,7 @@ func (c *Conn) runPostDataHook(data []byte) ([]byte, bool, error) { for _, v := range strings.Fields("USER PWD SHELL PATH") { cmd.Env = append(cmd.Env, v+"="+os.Getenv(v)) } - cmd.Env = append(cmd.Env, "REMOTE_ADDR="+c.conn.RemoteAddr().String()) + cmd.Env = append(cmd.Env, "REMOTE_ADDR="+c.remoteAddr.String()) cmd.Env = append(cmd.Env, "EHLO_DOMAIN="+sanitizeEHLODomain(c.ehloDomain)) cmd.Env = append(cmd.Env, "EHLO_DOMAIN_RAW="+c.ehloDomain) cmd.Env = append(cmd.Env, "MAIL_FROM="+c.mailFrom) @@ -1042,18 +1058,18 @@ func (c *Conn) AUTH(params string) (code int, msg string) { authOk, err := c.authr.Authenticate(user, domain, passwd) if err != nil { c.tr.Errorf("error authenticating %q@%q: %v", user, domain, err) - maillog.Auth(c.conn.RemoteAddr(), user+"@"+domain, false) + maillog.Auth(c.remoteAddr, user+"@"+domain, false) return 454, "4.7.0 Temporary authentication failure" } if authOk { c.authUser = user c.authDomain = domain c.completedAuth = true - maillog.Auth(c.conn.RemoteAddr(), user+"@"+domain, true) + maillog.Auth(c.remoteAddr, user+"@"+domain, true) return 235, "2.7.0 Authentication successful" } - maillog.Auth(c.conn.RemoteAddr(), user+"@"+domain, false) + maillog.Auth(c.remoteAddr, user+"@"+domain, false) return 535, "5.7.8 Incorrect user or password" } diff --git a/internal/smtpsrv/server.go b/internal/smtpsrv/server.go index bd66140..d19fa0f 100644 --- a/internal/smtpsrv/server.go +++ b/internal/smtpsrv/server.go @@ -45,6 +45,9 @@ type Server struct { // TLS config (including loaded certificates). tlsConfig *tls.Config + // Use HAProxy on incoming connections. + HAProxyEnabled bool + // Local domains. localDomains *set.String @@ -257,6 +260,7 @@ func (s *Server) serve(l net.Listener, mode SocketMode) { conn: conn, mode: mode, tlsConfig: s.tlsConfig, + haproxyEnabled: s.HAProxyEnabled, onTLS: mode.TLS, authr: s.authr, aliasesR: s.aliasesR, diff --git a/test/Dockerfile b/test/Dockerfile index 122808f..352966a 100644 --- a/test/Dockerfile +++ b/test/Dockerfile @@ -25,7 +25,8 @@ RUN apt-get install -y -q python3 msmtp # Install the optional packages for the integration tests. RUN apt-get install -y -q \ gettext-base dovecot-imapd \ - exim4-daemon-light + exim4-daemon-light \ + haproxy # Install sudo, needed for the docker entrypoint. RUN apt-get install -y -q sudo diff --git a/test/README.md b/test/README.md index 0617e14..74d9e7b 100644 --- a/test/README.md +++ b/test/README.md @@ -40,6 +40,8 @@ if the dependencies are not found: - `t-15-driusan_dkim` DKIM integration tests: - The `dkimsign dkimverify dkimkeygen` binaries, from [driusan/dkim](https://github.com/driusan/dkim) (no Debian package yet). +- `t-18-haproxy` HAProxy integration tests: + - `haproxy` For some tests, python >= 3.5 is required; they will be skipped if it's not available. diff --git a/test/t-18-haproxy/config/chasquid.conf b/test/t-18-haproxy/config/chasquid.conf new file mode 100644 index 0000000..377a5ef --- /dev/null +++ b/test/t-18-haproxy/config/chasquid.conf @@ -0,0 +1,12 @@ +smtp_address: ":2025" +submission_address: ":2587" +submission_over_tls_address: ":2465" +monitoring_address: ":2099" + +mail_delivery_agent_bin: "test-mda" +mail_delivery_agent_args: "%to%" + +data_dir: "../.data" +mail_log_path: "../.logs/mail_log" + +haproxy_incoming: true diff --git a/test/t-18-haproxy/content b/test/t-18-haproxy/content new file mode 100644 index 0000000..76a8b16 --- /dev/null +++ b/test/t-18-haproxy/content @@ -0,0 +1,4 @@ +Subject: Prueba desde el test + +Crece desde el test el futuro +Crece desde el test diff --git a/test/t-18-haproxy/haproxy.cfg b/test/t-18-haproxy/haproxy.cfg new file mode 100644 index 0000000..90dafad --- /dev/null +++ b/test/t-18-haproxy/haproxy.cfg @@ -0,0 +1,7 @@ +global + debug + +listen smtp-in + mode tcp + bind *:1025 + server srv1 localhost:2025 send-proxy diff --git a/test/t-18-haproxy/hosts b/test/t-18-haproxy/hosts new file mode 100644 index 0000000..2b9b623 --- /dev/null +++ b/test/t-18-haproxy/hosts @@ -0,0 +1 @@ +testserver localhost diff --git a/test/t-18-haproxy/msmtprc b/test/t-18-haproxy/msmtprc new file mode 100644 index 0000000..e39a7fc --- /dev/null +++ b/test/t-18-haproxy/msmtprc @@ -0,0 +1,14 @@ +account default + +host testserver +port 1025 + +tls on +tls_trust_file config/certs/testserver/fullchain.pem + +from user@testserver + +auth on +user user@testserver +password secretpassword + diff --git a/test/t-18-haproxy/run.sh b/test/t-18-haproxy/run.sh new file mode 100755 index 0000000..2bb7351 --- /dev/null +++ b/test/t-18-haproxy/run.sh @@ -0,0 +1,39 @@ +#!/bin/bash + +set -e +. $(dirname ${0})/../util/lib.sh + +init + +mkdir -p .logs + +if ! haproxy -v > /dev/null; then + skip "haproxy binary not found" + exit 0 +fi + +# Set a 2m timeout: if there are issues with haproxy, the wait tends to hang +# indefinitely, so an explicit timeout helps with test automation. +timeout 2m + +# Launch haproxy in the background, checking config first to fail fast in that +# case. +haproxy -f haproxy.cfg -c +haproxy -f haproxy.cfg > .logs/haproxy.log 2>&1 & + +generate_certs_for testserver +add_user user@testserver secretpassword +add_user someone@testserver secretpassword + +chasquid -v=2 --logfile=.logs/chasquid.log --config_dir=config & + +wait_until_ready 1025 # haproxy +wait_until_ready 2025 # chasquid + +run_msmtp someone@testserver < content + +wait_for_file .mail/someone@testserver + +mail_diff content .mail/someone@testserver + +success diff --git a/test/util/lib.sh b/test/util/lib.sh index f637655..b0bf3fe 100644 --- a/test/util/lib.sh +++ b/test/util/lib.sh @@ -123,6 +123,15 @@ function fexp() { ${UTILDIR}/fexp "$@" } +function timeout() { + MYPID=$$ + ( + sleep $1 + echo "timed out after $1, killing test" + kill -9 $MYPID + ) & +} + function success() { echo success }