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:
@@ -26,6 +26,7 @@ nav:
|
||||
- hooks.md
|
||||
- dovecot.md
|
||||
- dkim.md
|
||||
- haproxy.md
|
||||
- docker.md
|
||||
- flow.md
|
||||
- monitoring.md
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
32
docs/haproxy.md
Normal file
32
docs/haproxy.md
Normal 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
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
76
internal/haproxy/haproxy.go
Normal file
76
internal/haproxy/haproxy.go
Normal 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
|
||||
}
|
||||
97
internal/haproxy/haproxy_test.go
Normal file
97
internal/haproxy/haproxy_test.go
Normal 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
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
12
test/t-18-haproxy/config/chasquid.conf
Normal file
12
test/t-18-haproxy/config/chasquid.conf
Normal 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
|
||||
4
test/t-18-haproxy/content
Normal file
4
test/t-18-haproxy/content
Normal file
@@ -0,0 +1,4 @@
|
||||
Subject: Prueba desde el test
|
||||
|
||||
Crece desde el test el futuro
|
||||
Crece desde el test
|
||||
7
test/t-18-haproxy/haproxy.cfg
Normal file
7
test/t-18-haproxy/haproxy.cfg
Normal 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
1
test/t-18-haproxy/hosts
Normal file
@@ -0,0 +1 @@
|
||||
testserver localhost
|
||||
14
test/t-18-haproxy/msmtprc
Normal file
14
test/t-18-haproxy/msmtprc
Normal 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
39
test/t-18-haproxy/run.sh
Executable 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
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user