From 0a05d6f33437063548604f3b8cc50cc9dbe218ca Mon Sep 17 00:00:00 2001 From: John Weldon Date: Sat, 22 Feb 2025 11:38:35 -0700 Subject: [PATCH] target/remote: Add smtp_ports option for remote targets Allow ports other than 25 to be used for outgoing SMTP attempts. This change keeps the default behavior the same, but allows overriding it with configuration. --- dist/vim/syntax/maddy-conf.vim | 1 + docs/reference/targets/remote.md | 50 +++++++++++++++++++++++-------- internal/target/remote/connect.go | 38 ++++++++++++++++++----- internal/target/remote/remote.go | 3 ++ 4 files changed, 72 insertions(+), 20 deletions(-) diff --git a/dist/vim/syntax/maddy-conf.vim b/dist/vim/syntax/maddy-conf.vim index d59e7990..72790b68 100644 --- a/dist/vim/syntax/maddy-conf.vim +++ b/dist/vim/syntax/maddy-conf.vim @@ -198,6 +198,7 @@ syn keyword maddyModDir \ sig_expiry \ sign_fields \ sign_subdomains + \ smtp_ports \ softfail_action \ SOME_action \ source diff --git a/docs/reference/targets/remote.md b/docs/reference/targets/remote.md index 9a1b6061..53834b00 100644 --- a/docs/reference/targets/remote.md +++ b/docs/reference/targets/remote.md @@ -16,6 +16,7 @@ target.remote { ``` ### hostname _domain_ + Default: global directive value Hostname to use client greeting (EHLO/HELO command). Some servers require it to @@ -25,6 +26,7 @@ address, so it is better to set it to a domain that resolves to the server IP. --- ### limits { ... } + Default: no limits See ['limits' directive for SMTP endpoint](/reference/endpoints/smtp/#rate-concurrency-limiting). @@ -33,14 +35,16 @@ per-source/per-destination are as observed when message exits the server. --- -### local_ip _ip-address_ +### local*ip \_ip-address* + Default: empty Choose the local IP to bind for outbound SMTP connections. --- -### force_ipv4 _boolean_ +### force*ipv4 \_boolean* + Default: `false` Force resolving outbound SMTP domains to IPv4 addresses. Some server providers @@ -53,7 +57,17 @@ Warning: this may break sending outgoing mail to IPv6-only SMTP servers. --- -### connect_timeout _duration_ +### smtp_ports `string1 [ string2 [... stringN ]]` + +Default: `25` + +List of ports to try for outgoing SMTP connections, in the order to be +attempted. + +--- + +### connect*timeout \_duration* + Default: `5m` Timeout for TCP connection establishment. @@ -66,7 +80,8 @@ configures the former. The latter is not configurable and is hardcoded to be --- -### command_timeout _duration_ +### command*timeout \_duration* + Default: `5m` Timeout for any SMTP command (EHLO, MAIL, RCPT, DATA, etc). @@ -78,7 +93,8 @@ DATA. --- -### submission_timeout _duration_ +### submission*timeout \_duration* + Default: `12m` Time to wait after the entire message is sent (after "final dot"). @@ -88,13 +104,15 @@ RFC 5321 recommends 10 minutes. --- ### debug _boolean_ + Default: global directive value Enable verbose logging. --- -### requiretls_override _boolean_ +### requiretls*override \_boolean* + Default: `true` Allow local security policy to be disabled using 'TLS-Required' header field in @@ -104,7 +122,8 @@ to take effect (e.g. message should be queued using 'queue' module). --- -### relaxed_requiretls _boolean_ +### relaxed*requiretls \_boolean* + Default: `true` This option disables strict conformance with REQUIRETLS specification and @@ -116,7 +135,8 @@ there is only need to secure communication towards it and not beyond. --- -### conn_reuse_limit _integer_ +### conn*reuse_limit \_integer* + Default: `10` Amount of times the same SMTP connection can be used. @@ -124,14 +144,16 @@ Connections are never reused if the previous DATA command failed. --- -### conn_max_idle_count _integer_ +### conn*max_idle_count \_integer* + Default: `10` Max. amount of idle connections per recipient domains to keep in cache. --- -### conn_max_idle_time _integer_ +### conn*max_idle_time \_integer* + Default: `150` (2.5 min) Amount of time the idle connection is still considered potentially usable. @@ -141,6 +163,7 @@ Amount of time the idle connection is still considered potentially usable. ## Security policies ### mx_auth { ... } + Default: no policies 'remote' module implements a number of of schemes and protocols necessary to @@ -215,6 +238,7 @@ mtasts { ``` ### cache `fs` | `ram` + Default: `fs` Storage to use for MTA-STS cache. 'fs' is to use a filesystem directory, 'ram' @@ -224,7 +248,8 @@ It is recommended to use 'fs' since that will not discard the cache (and thus cause MTA-STS security to disappear) on server restart. However, using the RAM cache can make sense for high-load configurations with good uptime. -### fs_dir _directory_ +### fs*dir \_directory* + Default: `StateDirectory/mtasts_cache` Filesystem directory to use for policies caching if 'cache' is set to 'fs'. @@ -280,6 +305,7 @@ local_policy { Using `local_policy off` is equivalent to setting both directives to `none`. ### min_tls_level `none` | `encrypted` | `authenticated` + Default: `encrypted` Set the minimal TLS security level required for all outbound messages. @@ -287,9 +313,9 @@ Set the minimal TLS security level required for all outbound messages. See [Security levels](/seclevels) page for details. ### min_mx_level `none` | `mtasts` | `dnssec` + Default: `none` Set the minimal MX security level required for all outbound messages. See [Security levels](/seclevels) page for details. - diff --git a/internal/target/remote/connect.go b/internal/target/remote/connect.go index f9d317ef..34f36b47 100644 --- a/internal/target/remote/connect.go +++ b/internal/target/remote/connect.go @@ -83,6 +83,17 @@ func isVerifyError(err error) bool { // - tlsLevel TLS security level that was estabilished. // - tlsErr Error that prevented TLS from working if tlsLevel != TLSAuthenticated func (rd *remoteDelivery) connect(ctx context.Context, conn mxConn, host string, tlsCfg *tls.Config) (tlsLevel module.TLSLevel, tlsErr, err error) { + return rd.connectPort(ctx, conn, host, smtpPort, tlsCfg) +} + +// connectPort attempts to connect to the MX, first trying STARTTLS with X.509 +// verification but falling back to unauthenticated TLS or plaintext as +// necessary. +// +// Return values: +// - tlsLevel TLS security level that was estabilished. +// - tlsErr Error that prevented TLS from working if tlsLevel != TLSAuthenticated +func (rd *remoteDelivery) connectPort(ctx context.Context, conn mxConn, host string, port string, tlsCfg *tls.Config) (tlsLevel module.TLSLevel, tlsErr, err error) { tlsLevel = module.TLSAuthenticated if rd.rt.tlsConfig != nil { tlsCfg = rd.rt.tlsConfig.Clone() @@ -96,7 +107,7 @@ retry: // TLS errors separately hence starttls=false. _, err = conn.Connect(ctx, config.Endpoint{ Host: host, - Port: smtpPort, + Port: port, }, false, nil) if err != nil { return module.TLSNone, nil, err @@ -151,6 +162,10 @@ retry: } func (rd *remoteDelivery) attemptMX(ctx context.Context, conn *mxConn, record *net.MX) error { + return rd.attemptMXWithPort(ctx, conn, record, smtpPort) +} + +func (rd *remoteDelivery) attemptMXWithPort(ctx context.Context, conn *mxConn, record *net.MX, port string) error { mxLevel := module.MXNone connCtx, cancel := context.WithCancel(ctx) @@ -169,7 +184,7 @@ func (rd *remoteDelivery) attemptMX(ctx context.Context, conn *mxConn, record *n p.PrepareConn(ctx, record.Host) } - tlsLevel, tlsErr, err := rd.connect(connCtx, *conn, record.Host, rd.rt.tlsConfig) + tlsLevel, tlsErr, err := rd.connectPort(connCtx, *conn, record.Host, port, rd.rt.tlsConfig) if err != nil { return err } @@ -316,7 +331,12 @@ func (rd *remoteDelivery) newConn(ctx context.Context, domain string) (*mxConn, conn.dnssecOk = dnssecOk var lastErr error + ports := rd.rt.smtpPorts + if len(ports) == 0 { + ports = []string{smtpPort} + } region = trace.StartRegion(ctx, "remote/Connect+TLS") +recordsLoop: for _, record := range records { if record.Host == "." { return nil, &exterrors.SMTPError{ @@ -326,14 +346,16 @@ func (rd *remoteDelivery) newConn(ctx context.Context, domain string) (*mxConn, } } - if err := rd.attemptMX(ctx, &conn, record); err != nil { - if len(records) != 0 { - rd.Log.Error("cannot use MX", err, "remote_server", record.Host, "domain", domain) + for _, port := range ports { + if err := rd.attemptMXWithPort(ctx, &conn, record, port); err != nil { + if len(records) != 0 { + rd.Log.Error("cannot use MX", err, "remote_server", record.Host, "remote_port", port, "domain", domain) + } + lastErr = err + continue } - lastErr = err - continue + break recordsLoop } - break } region.End() diff --git a/internal/target/remote/remote.go b/internal/target/remote/remote.go index 79b683fd..078a7afc 100644 --- a/internal/target/remote/remote.go +++ b/internal/target/remote/remote.go @@ -66,6 +66,8 @@ type Target struct { ipv4 bool tlsConfig *tls.Config + smtpPorts []string + resolver dns.Resolver dialer func(ctx context.Context, network, addr string) (net.Conn, error) extResolver *dns.ExtResolver @@ -112,6 +114,7 @@ func (rt *Target) Configure(inlineArgs []string, cfg *config.Map) error { cfg.String("local_ip", false, false, "", &rt.localIP) cfg.Bool("force_ipv4", false, false, &rt.ipv4) cfg.Bool("debug", true, false, &rt.Log.Debug) + cfg.StringList("smtp_ports", false, false, []string{smtpPort}, &rt.smtpPorts) cfg.Custom("tls_client", true, false, func() (interface{}, error) { return &tls.Config{}, nil }, tls2.TLSClientBlock, &rt.tlsConfig)