1
0
mirror of https://blitiri.com.ar/repos/chasquid synced 2025-12-17 14:37:02 +00:00

Implement HAProxy protocol support

This patch implements support for incoming connections wrapped in the
HAProxy protocol v1.

This is useful when running chasquid behind a HAProxy server, as it
needs the original source IP to perform SPF checks.

This patch is a reimplementation of one originally provided by Denys
Vitali in pull request #15, except the logic for the protocol handling
is moved to a new package, and the smtpsrv.Conn handling of the source
IP is simplified.

It is marked as experimental for now, since we want to give it a bit
more exposure just in case the option/api needs adjustment.

Thanks a lot to Denys Vitali (@denysvitali in github) for sending the
original patch for this, and helping test it!
This commit is contained in:
Alberto Bertogli
2020-11-12 22:00:46 +00:00
parent c9d3ba0ca0
commit e79586a014
22 changed files with 389 additions and 24 deletions

View File

@@ -26,6 +26,7 @@ nav:
- hooks.md - hooks.md
- dovecot.md - dovecot.md
- dkim.md - dkim.md
- haproxy.md
- docker.md - docker.md
- flow.md - flow.md
- monitoring.md - monitoring.md

View File

@@ -100,6 +100,7 @@ func main() {
s.Hostname = conf.Hostname s.Hostname = conf.Hostname
s.MaxDataSize = conf.MaxDataSizeMb * 1024 * 1024 s.MaxDataSize = conf.MaxDataSizeMb * 1024 * 1024
s.HookPath = "hooks/" s.HookPath = "hooks/"
s.HAProxyEnabled = conf.HaproxyIncoming
s.SetAliasesConfig(conf.SuffixSeparators, conf.DropCharacters) s.SetAliasesConfig(conf.SuffixSeparators, conf.DropCharacters)

32
docs/haproxy.md Normal file
View File

@@ -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

View File

@@ -133,7 +133,7 @@
.\" ======================================================================== .\" ========================================================================
.\" .\"
.IX Title "chasquid.conf 5" .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 .\" For nroff, turn off justification. Always turn off hyphenation; it makes
.\" way too many mistakes in technical documents. .\" way too many mistakes in technical documents.
.if n .ad l .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 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 overridden using the \f(CW\*(C`dovecot_userdb_path\*(C'\fR and \f(CW\*(C`dovecot_client_path\*(C'\fR if
needed. 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" .SH "SEE ALSO"
.IX Header "SEE ALSO" .IX Header "SEE ALSO"
\&\fBchasquid\fR\|(1) \&\fBchasquid\fR\|(1)

View File

@@ -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 overridden using the C<dovecot_userdb_path> and C<dovecot_client_path> if
needed. 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 =back
=head1 SEE ALSO =head1 SEE ALSO

View File

@@ -87,3 +87,11 @@
# Default: "" (autodetect) # Default: "" (autodetect)
#dovecot_userdb_path: "" #dovecot_userdb_path: ""
#dovecot_client_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

View File

@@ -123,6 +123,10 @@ func override(c, o *Config) {
if o.DovecotClientPath != "" { if o.DovecotClientPath != "" {
c.DovecotClientPath = o.DovecotClientPath c.DovecotClientPath = o.DovecotClientPath
} }
if o.HaproxyIncoming {
c.HaproxyIncoming = true
}
} }
// LogConfig logs the given configuration, in a human-friendly way. // 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(" Mail log: %s", c.MailLogPath)
log.Infof(" Dovecot auth: %v (%q, %q)", log.Infof(" Dovecot auth: %v (%q, %q)",
c.DovecotAuth, c.DovecotUserdbPath, c.DovecotClientPath) c.DovecotAuth, c.DovecotUserdbPath, c.DovecotClientPath)
log.Infof(" HAProxy incoming: %v", c.HaproxyIncoming)
} }

View File

@@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.23.0 // protoc-gen-go v1.25.0
// protoc v3.12.3 // protoc v3.12.3
// source: config.proto // source: config.proto
@@ -108,6 +108,10 @@ type Config struct {
// is not, we will try to autodetect it. // is not, we will try to autodetect it.
// Example: /var/run/dovecot/auth-client // Example: /var/run/dovecot/auth-client
DovecotClientPath string `protobuf:"bytes,15,opt,name=dovecot_client_path,json=dovecotClientPath,proto3" json:"dovecot_client_path,omitempty"` 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() { func (x *Config) Reset() {
@@ -247,10 +251,17 @@ func (x *Config) GetDovecotClientPath() string {
return "" return ""
} }
func (x *Config) GetHaproxyIncoming() bool {
if x != nil {
return x.HaproxyIncoming
}
return false
}
var File_config_proto protoreflect.FileDescriptor var File_config_proto protoreflect.FileDescriptor
var file_config_proto_rawDesc = []byte{ 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, 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, 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, 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, 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, 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, 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, 0x6e, 0x74, 0x50, 0x61, 0x74, 0x68, 0x12, 0x29, 0x0a, 0x10, 0x68, 0x61, 0x70, 0x72, 0x6f, 0x78,
0x69, 0x2e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x72, 0x2f, 0x67, 0x6f, 0x2f, 0x63, 0x68, 0x61, 0x73, 0x79, 0x5f, 0x69, 0x6e, 0x63, 0x6f, 0x6d, 0x69, 0x6e, 0x67, 0x18, 0x10, 0x20, 0x01, 0x28, 0x08,
0x71, 0x75, 0x69, 0x64, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x63, 0x6f, 0x52, 0x0f, 0x68, 0x61, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x49, 0x6e, 0x63, 0x6f, 0x6d, 0x69, 0x6e,
0x6e, 0x66, 0x69, 0x67, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 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 ( var (

View File

@@ -95,4 +95,9 @@ message Config {
// is not, we will try to autodetect it. // is not, we will try to autodetect it.
// Example: /var/run/dovecot/auth-client // Example: /var/run/dovecot/auth-client
string dovecot_client_path = 15; 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;
} }

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -25,6 +25,7 @@ import (
"blitiri.com.ar/go/chasquid/internal/domaininfo" "blitiri.com.ar/go/chasquid/internal/domaininfo"
"blitiri.com.ar/go/chasquid/internal/envelope" "blitiri.com.ar/go/chasquid/internal/envelope"
"blitiri.com.ar/go/chasquid/internal/expvarom" "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/maillog"
"blitiri.com.ar/go/chasquid/internal/normalize" "blitiri.com.ar/go/chasquid/internal/normalize"
"blitiri.com.ar/go/chasquid/internal/queue" "blitiri.com.ar/go/chasquid/internal/queue"
@@ -104,6 +105,7 @@ type Conn struct {
conn net.Conn conn net.Conn
mode SocketMode mode SocketMode
tlsConnState *tls.ConnectionState tlsConnState *tls.ConnectionState
remoteAddr net.Addr
// Reader and text writer, so we can control limits. // Reader and text writer, so we can control limits.
reader *bufio.Reader reader *bufio.Reader
@@ -158,6 +160,9 @@ type Conn struct {
// Time we wait for network operations. // Time we wait for network operations.
commandTimeout time.Duration commandTimeout time.Duration
// Enable HAProxy on incoming connections.
haproxyEnabled bool
} }
// Close the connection. // Close the connection.
@@ -199,6 +204,17 @@ func (c *Conn) Handle() {
c.reader = bufio.NewReader(c.conn) c.reader = bufio.NewReader(c.conn)
c.writer = bufio.NewWriter(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) c.printfLine("220 %s ESMTP chasquid", c.hostname)
var cmd, params string 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) c.spfResult, c.spfError = c.checkSPF(addr)
if c.spfResult == spf.Fail { if c.spfResult == spf.Fail {
// https://tools.ietf.org/html/rfc7208#section-8.4 // 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)) fmt.Sprintf("failed SPF: %v", c.spfError))
return 550, fmt.Sprintf( return 550, fmt.Sprintf(
"5.7.23 SPF check failed: %v", c.spfError) "5.7.23 SPF check failed: %v", c.spfError)
} }
if !c.secLevelCheck(addr) { if !c.secLevelCheck(addr) {
maillog.Rejected(c.conn.RemoteAddr(), addr, nil, maillog.Rejected(c.remoteAddr, addr, nil,
"security level check failed") "security level check failed")
return 550, "5.7.3 Security level check failed" return 550, "5.7.3 Security level check failed"
} }
addr, err = normalize.DomainToUnicode(addr) addr, err = normalize.DomainToUnicode(addr)
if err != nil { if err != nil {
maillog.Rejected(c.conn.RemoteAddr(), addr, nil, maillog.Rejected(c.remoteAddr, addr, nil,
fmt.Sprintf("malformed address: %v", err)) fmt.Sprintf("malformed address: %v", err))
return 501, "5.1.8 Malformed sender domain (IDNA conversion failed)" 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 return "", nil
} }
if tcp, ok := c.conn.RemoteAddr().(*net.TCPAddr); ok { if tcp, ok := c.remoteAddr.(*net.TCPAddr); ok {
res, err := spf.CheckHostWithSender( res, err := spf.CheckHostWithSender(
tcp.IP, envelope.DomainOf(addr), addr) 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) localDst := envelope.DomainIn(addr, c.localDomains)
if !localDst && !c.completedAuth { if !localDst && !c.completedAuth {
maillog.Rejected(c.conn.RemoteAddr(), c.mailFrom, []string{addr}, maillog.Rejected(c.remoteAddr, c.mailFrom, []string{addr},
"relay not allowed") "relay not allowed")
return 503, "5.7.1 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 { if localDst {
addr, err = normalize.Addr(addr) addr, err = normalize.Addr(addr)
if err != nil { 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)) fmt.Sprintf("invalid address: %v", err))
return 550, "5.1.3 Destination address is invalid" return 550, "5.1.3 Destination address is invalid"
} }
if !c.userExists(addr) { 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") "local user does not exist")
return 550, "5.1.1 Destination address is unknown (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)) c.tr.Debugf("-> ... %d bytes of data", len(c.data))
if err := checkData(c.data); err != nil { 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() 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) hookOut, permanent, err := c.runPostDataHook(c.data)
if err != nil { 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 { if permanent {
return 554, err.Error() 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) 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, // It is very important that we reset the envelope before returning,
// so clients can send other emails right away without needing to RSET. // 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 // and then the given EHLO domain for convenience and
// troubleshooting. // troubleshooting.
v += fmt.Sprintf("from [%s] (%s)\n", 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) 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) hookResults.Add("post-data:skip", 1)
return nil, false, nil 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() defer tr.Finish()
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute) 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") { for _, v := range strings.Fields("USER PWD SHELL PATH") {
cmd.Env = append(cmd.Env, v+"="+os.Getenv(v)) 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="+sanitizeEHLODomain(c.ehloDomain))
cmd.Env = append(cmd.Env, "EHLO_DOMAIN_RAW="+c.ehloDomain) cmd.Env = append(cmd.Env, "EHLO_DOMAIN_RAW="+c.ehloDomain)
cmd.Env = append(cmd.Env, "MAIL_FROM="+c.mailFrom) 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) authOk, err := c.authr.Authenticate(user, domain, passwd)
if err != nil { if err != nil {
c.tr.Errorf("error authenticating %q@%q: %v", user, domain, err) 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" return 454, "4.7.0 Temporary authentication failure"
} }
if authOk { if authOk {
c.authUser = user c.authUser = user
c.authDomain = domain c.authDomain = domain
c.completedAuth = true 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" 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" return 535, "5.7.8 Incorrect user or password"
} }

View File

@@ -45,6 +45,9 @@ type Server struct {
// TLS config (including loaded certificates). // TLS config (including loaded certificates).
tlsConfig *tls.Config tlsConfig *tls.Config
// Use HAProxy on incoming connections.
HAProxyEnabled bool
// Local domains. // Local domains.
localDomains *set.String localDomains *set.String
@@ -257,6 +260,7 @@ func (s *Server) serve(l net.Listener, mode SocketMode) {
conn: conn, conn: conn,
mode: mode, mode: mode,
tlsConfig: s.tlsConfig, tlsConfig: s.tlsConfig,
haproxyEnabled: s.HAProxyEnabled,
onTLS: mode.TLS, onTLS: mode.TLS,
authr: s.authr, authr: s.authr,
aliasesR: s.aliasesR, aliasesR: s.aliasesR,

View File

@@ -25,7 +25,8 @@ RUN apt-get install -y -q python3 msmtp
# Install the optional packages for the integration tests. # Install the optional packages for the integration tests.
RUN apt-get install -y -q \ RUN apt-get install -y -q \
gettext-base dovecot-imapd \ gettext-base dovecot-imapd \
exim4-daemon-light exim4-daemon-light \
haproxy
# Install sudo, needed for the docker entrypoint. # Install sudo, needed for the docker entrypoint.
RUN apt-get install -y -q sudo RUN apt-get install -y -q sudo

View File

@@ -40,6 +40,8 @@ if the dependencies are not found:
- `t-15-driusan_dkim` DKIM integration tests: - `t-15-driusan_dkim` DKIM integration tests:
- The `dkimsign dkimverify dkimkeygen` binaries, from - The `dkimsign dkimverify dkimkeygen` binaries, from
[driusan/dkim](https://github.com/driusan/dkim) (no Debian package yet). [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 For some tests, python >= 3.5 is required; they will be skipped if it's not
available. available.

View File

@@ -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

View File

@@ -0,0 +1,4 @@
Subject: Prueba desde el test
Crece desde el test el futuro
Crece desde el test

View File

@@ -0,0 +1,7 @@
global
debug
listen smtp-in
mode tcp
bind *:1025
server srv1 localhost:2025 send-proxy

1
test/t-18-haproxy/hosts Normal file
View File

@@ -0,0 +1 @@
testserver localhost

14
test/t-18-haproxy/msmtprc Normal file
View File

@@ -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

39
test/t-18-haproxy/run.sh Executable file
View File

@@ -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

View File

@@ -123,6 +123,15 @@ function fexp() {
${UTILDIR}/fexp "$@" ${UTILDIR}/fexp "$@"
} }
function timeout() {
MYPID=$$
(
sleep $1
echo "timed out after $1, killing test"
kill -9 $MYPID
) &
}
function success() { function success() {
echo success echo success
} }