Skip to content

Commit

Permalink
Merge commit 'be570d1c7d3de0ddacb011b6411a302d7f7e9f9e'
Browse files Browse the repository at this point in the history
from github PR #153
  • Loading branch information
mjl- committed Apr 13, 2024
2 parents f4b6e14 + be570d1 commit 73381d2
Show file tree
Hide file tree
Showing 14 changed files with 124 additions and 36 deletions.
16 changes: 12 additions & 4 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -222,10 +222,11 @@ type WebService struct {
// be non-nil. The non-nil field represents the type of transport. For a
// transport with all fields nil, regular email delivery is done.
type Transport struct {
Submissions *TransportSMTP `sconf:"optional" sconf-doc:"Submission SMTP over a TLS connection to submit email to a remote queue."`
Submission *TransportSMTP `sconf:"optional" sconf-doc:"Submission SMTP over a plain TCP connection (possibly with STARTTLS) to submit email to a remote queue."`
SMTP *TransportSMTP `sconf:"optional" sconf-doc:"SMTP over a plain connection (possibly with STARTTLS), typically for old-fashioned unauthenticated relaying to a remote queue."`
Socks *TransportSocks `sconf:"optional" sconf-doc:"Like regular direct delivery, but makes outgoing connections through a SOCKS proxy."`
Submissions *TransportSMTP `sconf:"optional" sconf-doc:"Submission SMTP over a TLS connection to submit email to a remote queue."`
Submission *TransportSMTP `sconf:"optional" sconf-doc:"Submission SMTP over a plain TCP connection (possibly with STARTTLS) to submit email to a remote queue."`
SMTP *TransportSMTP `sconf:"optional" sconf-doc:"SMTP over a plain connection (possibly with STARTTLS), typically for old-fashioned unauthenticated relaying to a remote queue."`
Socks *TransportSocks `sconf:"optional" sconf-doc:"Like regular direct delivery, but makes outgoing connections through a SOCKS proxy."`
Direct *TransportDirect `sconf:"optional" sconf-doc:"Like regular direct delivery, but allows to tweak outgoing connections."`
}

// TransportSMTP delivers messages by "submission" (SMTP, typically
Expand Down Expand Up @@ -262,6 +263,13 @@ type TransportSocks struct {
Hostname dns.Domain `sconf:"-" json:"-"` // Parsed form of RemoteHostname
}

type TransportDirect struct {
DisableIPv4 bool `sconf:"optional" sconf-doc:"If set, outgoing SMTP connections will *NOT* use IPv4 addresses to connect to remote SMTP servers."`
DisableIPv6 bool `sconf:"optional" sconf-doc:"If set, outgoing SMTP connections will *NOT* use IPv6 addresses to connect to remote SMTP servers."`

IPFamily string `sconf:"-" json:"-"`
}

type Domain struct {
Description string `sconf:"optional" sconf-doc:"Free-form description of domain."`
ClientSettingsDomain string `sconf:"optional" sconf-doc:"Hostname for client settings instead of the mail server hostname. E.g. mail.<domain>. For future migration to another mail operator without requiring all clients to update their settings, it is convenient to have client settings that reference a subdomain of the hosted domain instead of the hostname of the server where the mail is currently hosted. If empty, the hostname of the mail server is used for client configurations. Unicode name."`
Expand Down
12 changes: 12 additions & 0 deletions config/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -638,6 +638,18 @@ See https://pkg.go.dev/github.com/mjl-/sconf for details.
# typically the hostname of the host in the Address field.
RemoteHostname:
# Like regular direct delivery, but allows to tweak outgoing connections.
# (optional)
Direct:
# If set, outgoing SMTP connections will *NOT* use IPv4 addresses to connect to
# remote SMTP servers. (optional)
DisableIPv4: false
# If set, outgoing SMTP connections will *NOT* use IPv6 addresses to connect to
# remote SMTP servers. (optional)
DisableIPv6: false
# Do not send DMARC reports (aggregate only). By default, aggregate reports on
# DMARC evaluations are sent to domains if their DMARC policy requests them.
# Reports are sent at whole hours, with a minimum of 1 hour and maximum of 24
Expand Down
2 changes: 1 addition & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -1505,7 +1505,7 @@ sharing most of its code.

log.Printf("attempting to connect to %s", host)

authentic, expandedAuthentic, expandedHost, ips, _, err := smtpclient.GatherIPs(ctxbg, c.log.Logger, resolver, host, dialedIPs)
authentic, expandedAuthentic, expandedHost, ips, _, err := smtpclient.GatherIPs(ctxbg, c.log.Logger, resolver, "ip", host, dialedIPs)
if err != nil {
log.Printf("resolving ips for %s: %v, skipping", host, err)
continue
Expand Down
17 changes: 17 additions & 0 deletions mox-/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -929,6 +929,19 @@ func PrepareStaticConfig(ctx context.Context, log mlog.Log, configFile string, c
}
}

checkTransportDirect := func(name string, t *config.TransportDirect) {
if t.DisableIPv4 && t.DisableIPv6 {
addErrorf("transport %s: both IPv4 and IPv6 are disabled, enable at least one", name)
}
t.IPFamily = "ip"
if t.DisableIPv4 {
t.IPFamily = "ip6"
}
if t.DisableIPv6 {
t.IPFamily = "ip4"
}
}

for name, t := range c.Transports {
n := 0
if t.Submissions != nil {
Expand All @@ -947,6 +960,10 @@ func PrepareStaticConfig(ctx context.Context, log mlog.Log, configFile string, c
n++
checkTransportSocks(name, t.Socks)
}
if t.Direct != nil {
n++
checkTransportDirect(name, t.Direct)
}
if n > 1 {
addErrorf("transport %s: cannot have multiple methods in a transport", name)
}
Expand Down
19 changes: 14 additions & 5 deletions queue/direct.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"github.com/mjl-/adns"
"github.com/mjl-/bstore"

"github.com/mjl-/mox/config"
"github.com/mjl-/mox/dns"
"github.com/mjl-/mox/dsn"
"github.com/mjl-/mox/mlog"
Expand Down Expand Up @@ -110,7 +111,7 @@ type msgResp struct {
// domain (MTA-STS), its policy type can be empty, in which case there is no
// information (e.g. internal failure). hostResults are per-host details (DANE, one
// per MX target).
func deliverDirect(qlog mlog.Log, resolver dns.Resolver, dialer smtpclient.Dialer, ourHostname dns.Domain, transportName string, msgs []*Msg, backoff time.Duration) (recipientDomainResult tlsrpt.Result, hostResults []tlsrpt.Result) {
func deliverDirect(qlog mlog.Log, resolver dns.Resolver, dialer smtpclient.Dialer, ourHostname dns.Domain, transportName string, transportDirect *config.TransportDirect, msgs []*Msg, backoff time.Duration) (recipientDomainResult tlsrpt.Result, hostResults []tlsrpt.Result) {
// High-level approach:
// - Resolve domain to deliver to (CNAME), and determine hosts to try to deliver to (MX)
// - Get MTA-STS policy for domain (optional). If present, only deliver to its
Expand Down Expand Up @@ -252,7 +253,7 @@ func deliverDirect(qlog mlog.Log, resolver dns.Resolver, dialer smtpclient.Diale
msgResps[i] = &msgResp{msg: msgs[i]}
}

result := deliverHost(nqlog, resolver, dialer, ourHostname, transportName, h, enforceMTASTS, haveMX, origNextHopAuthentic, origNextHop, expandedNextHopAuthentic, expandedNextHop, msgResps, tlsMode, tlsPKIX, &recipientDomainResult)
result := deliverHost(nqlog, resolver, dialer, ourHostname, transportName, transportDirect, h, enforceMTASTS, haveMX, origNextHopAuthentic, origNextHop, expandedNextHopAuthentic, expandedNextHop, msgResps, tlsMode, tlsPKIX, &recipientDomainResult)

var zerotype tlsrpt.PolicyType
if result.hostResult.Policy.Type != zerotype {
Expand All @@ -279,7 +280,7 @@ func deliverDirect(qlog mlog.Log, resolver dns.Resolver, dialer smtpclient.Diale
slog.Bool("enforcemtasts", enforceMTASTS),
slog.Bool("tlsdane", result.tlsDANE),
slog.Any("requiretls", m0.RequireTLS))
result = deliverHost(nqlog, resolver, dialer, ourHostname, transportName, h, enforceMTASTS, haveMX, origNextHopAuthentic, origNextHop, expandedNextHopAuthentic, expandedNextHop, msgResps, smtpclient.TLSSkip, false, &tlsrpt.Result{})
result = deliverHost(nqlog, resolver, dialer, ourHostname, transportName, transportDirect, h, enforceMTASTS, haveMX, origNextHopAuthentic, origNextHop, expandedNextHopAuthentic, expandedNextHop, msgResps, smtpclient.TLSSkip, false, &tlsrpt.Result{})
}

remoteMTA = dsn.NameIP{Name: h.XString(false), IP: remoteIP}
Expand Down Expand Up @@ -375,7 +376,7 @@ type deliverResult struct {
//
// deliverHost may send a message multiple times: if the server doesn't accept
// multiple recipients for a message.
func deliverHost(log mlog.Log, resolver dns.Resolver, dialer smtpclient.Dialer, ourHostname dns.Domain, transportName string, host dns.IPDomain, enforceMTASTS, haveMX, origNextHopAuthentic bool, origNextHop dns.Domain, expandedNextHopAuthentic bool, expandedNextHop dns.Domain, msgResps []*msgResp, tlsMode smtpclient.TLSMode, tlsPKIX bool, recipientDomainResult *tlsrpt.Result) (result deliverResult) {
func deliverHost(log mlog.Log, resolver dns.Resolver, dialer smtpclient.Dialer, ourHostname dns.Domain, transportName string, transportDirect *config.TransportDirect, host dns.IPDomain, enforceMTASTS, haveMX, origNextHopAuthentic bool, origNextHop dns.Domain, expandedNextHopAuthentic bool, expandedNextHop dns.Domain, msgResps []*msgResp, tlsMode smtpclient.TLSMode, tlsPKIX bool, recipientDomainResult *tlsrpt.Result) (result deliverResult) {
// About attempting delivery to multiple addresses of a host: ../rfc/5321:3898

m0 := msgResps[0].msg
Expand Down Expand Up @@ -451,7 +452,14 @@ func deliverHost(log mlog.Log, resolver dns.Resolver, dialer smtpclient.Dialer,
}

metricDestinations.Inc()
authentic, expandedAuthentic, expandedHost, ips, dualstack, err := smtpclient.GatherIPs(ctx, log.Logger, resolver, host, m0.DialedIPs)
network := "ip"
if transportDirect != nil {
if network != transportDirect.IPFamily {
log.Debug("set custom IP network family for direct transport", slog.Any("network", transportDirect.IPFamily))
network = transportDirect.IPFamily
}
}
authentic, expandedAuthentic, expandedHost, ips, dualstack, err := smtpclient.GatherIPs(ctx, log.Logger, resolver, network, host, m0.DialedIPs)
destAuthentic := err == nil && authentic && origNextHopAuthentic && (!haveMX || expandedNextHopAuthentic) && host.IsDomain()
if !destAuthentic {
log.Debugx("not attempting verification with dane", err, slog.Bool("authentic", authentic), slog.Bool("expandedauthentic", expandedAuthentic))
Expand Down Expand Up @@ -645,6 +653,7 @@ func deliverHost(log mlog.Log, resolver dns.Resolver, dialer smtpclient.Dialer,
// attempt and remote has both IPv4 and IPv6, we'll give it
// another try. Our first IP may be in a block list, the address for
// the other family perhaps is not.

if cerr.Permanent && m0.Attempts == 1 && dualstack && strings.HasPrefix(cerr.Secode, "7.") {
cerr.Permanent = false
}
Expand Down
2 changes: 1 addition & 1 deletion queue/queue.go
Original file line number Diff line number Diff line change
Expand Up @@ -1075,7 +1075,7 @@ func deliver(log mlog.Log, resolver dns.Resolver, m Msg) {
}
ourHostname = transport.Socks.Hostname
}
recipientDomainResult, hostResults = deliverDirect(qlog, resolver, dialer, ourHostname, transportName, msgs, backoff)
recipientDomainResult, hostResults = deliverDirect(qlog, resolver, dialer, ourHostname, transportName, transport.Direct, msgs, backoff)
}
}

Expand Down
2 changes: 1 addition & 1 deletion queue/submit.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ func deliverSubmit(qlog mlog.Log, resolver dns.Resolver, dialer smtpclient.Diale
msgs[0].DialedIPs = map[string][]net.IP{}
m0 = msgs[0]
}
_, _, _, ips, _, err := smtpclient.GatherIPs(dialctx, qlog.Logger, resolver, dns.IPDomain{Domain: transport.DNSHost}, m0.DialedIPs)
_, _, _, ips, _, err := smtpclient.GatherIPs(dialctx, qlog.Logger, resolver, "ip", dns.IPDomain{Domain: transport.DNSHost}, m0.DialedIPs)
var conn net.Conn
if err == nil {
conn, _, err = smtpclient.Dial(dialctx, qlog.Logger, dialer, dns.IPDomain{Domain: transport.DNSHost}, ips, port, m0.DialedIPs, mox.Conf.Static.SpecifiedSMTPListenIPs)
Expand Down
4 changes: 2 additions & 2 deletions smtpclient/dial_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ func TestDialHost(t *testing.T) {
}

dialedIPs := map[string][]net.IP{}
_, _, _, ips, dualstack, err := GatherIPs(ctxbg, log.Logger, resolver, ipdomain("dualstack.example"), dialedIPs)
_, _, _, ips, dualstack, err := GatherIPs(ctxbg, log.Logger, resolver, "ip", ipdomain("dualstack.example"), dialedIPs)
if err != nil || !reflect.DeepEqual(ips, []net.IP{net.ParseIP("10.0.0.1"), net.ParseIP("2001:db8::1")}) || !dualstack {
t.Fatalf("expected err nil, address 10.0.0.1,2001:db8::1, dualstack true, got %v %v %v", err, ips, dualstack)
}
Expand All @@ -46,7 +46,7 @@ func TestDialHost(t *testing.T) {
t.Fatalf("expected err nil, address 10.0.0.1, dualstack true, got %v %v %v", err, ip, dualstack)
}

_, _, _, ips, dualstack, err = GatherIPs(ctxbg, log.Logger, resolver, ipdomain("dualstack.example"), dialedIPs)
_, _, _, ips, dualstack, err = GatherIPs(ctxbg, log.Logger, resolver, "ip", ipdomain("dualstack.example"), dialedIPs)
if err != nil || !reflect.DeepEqual(ips, []net.IP{net.ParseIP("2001:db8::1"), net.ParseIP("10.0.0.1")}) || !dualstack {
t.Fatalf("expected err nil, address 2001:db8::1,10.0.0.1, dualstack true, got %v %v %v", err, ips, dualstack)
}
Expand Down
8 changes: 4 additions & 4 deletions smtpclient/gather.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ func GatherDestinations(ctx context.Context, elog *slog.Logger, resolver dns.Res
// GatherIPs looks up the IPs to try for connecting to host, with the IPs ordered
// to take previous attempts into account. For use with DANE, the CNAME-expanded
// name is returned, and whether the DNS responses were authentic.
func GatherIPs(ctx context.Context, elog *slog.Logger, resolver dns.Resolver, host dns.IPDomain, dialedIPs map[string][]net.IP) (authentic bool, expandedAuthentic bool, expandedHost dns.Domain, ips []net.IP, dualstack bool, rerr error) {
func GatherIPs(ctx context.Context, elog *slog.Logger, resolver dns.Resolver, network string, host dns.IPDomain, dialedIPs map[string][]net.IP) (authentic bool, expandedAuthentic bool, expandedHost dns.Domain, ips []net.IP, dualstack bool, rerr error) {
log := mlog.New("smtpclient", elog)

if len(host.IP) > 0 {
Expand Down Expand Up @@ -216,16 +216,16 @@ func GatherIPs(ctx context.Context, elog *slog.Logger, resolver dns.Resolver, ho
}
}

ipaddrs, result, err := resolver.LookupIPAddr(ctx, name)
ipaddrs, result, err := resolver.LookupIP(ctx, network, name)
authentic = authentic && result.Authentic
expandedAuthentic = expandedAuthentic && result.Authentic
if err != nil || len(ipaddrs) == 0 {
return authentic, expandedAuthentic, expandedHost, nil, false, fmt.Errorf("looking up %q: %w", name, err)
}
var have4, have6 bool
for _, ipaddr := range ipaddrs {
ips = append(ips, ipaddr.IP)
if ipaddr.IP.To4() == nil {
ips = append(ips, ipaddr)
if ipaddr.To4() == nil {
have6 = true
} else {
have4 = true
Expand Down
30 changes: 17 additions & 13 deletions smtpclient/gather_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,16 +155,16 @@ func TestGatherIPs(t *testing.T) {
"temperror-cname.example.": "absent.example.",
},
Fail: []string{
"host temperror-a.example.",
"ip temperror-a.example.",
"cname temperror-cname.example.",
},
Inauthentic: []string{"cname cnameinauthentic.example."},
}

test := func(host dns.IPDomain, expAuthic, expAuthicExp bool, expHostExp dns.Domain, expIPs []net.IP, expErr any) {
test := func(host dns.IPDomain, expAuthic, expAuthicExp bool, expHostExp dns.Domain, expIPs []net.IP, expErr any, network string) {
t.Helper()

authic, authicExp, hostExp, ips, _, err := GatherIPs(ctxbg, log.Logger, resolver, host, nil)
authic, authicExp, hostExp, ips, _, err := GatherIPs(ctxbg, log.Logger, resolver, network, host, nil)
if (err == nil) != (expErr == nil) || err != nil && !(errors.Is(err, expErr.(error)) || errors.As(err, &expErr)) {
// todo: could also check the individual errors?
t.Fatalf("gather hosts: %v, expected %v", err, expErr)
Expand All @@ -191,18 +191,22 @@ func TestGatherIPs(t *testing.T) {
authic := i == 1
resolver.AllAuthentic = authic

test(ipdomain("host1.example"), authic, authic, zerohost, ips("10.0.0.1"), nil)
test(ipdomain("host2.example"), authic, authic, zerohost, ips("10.0.0.2", "2001:db8::1"), nil)
test(ipdomain("cname-to-inauthentic.example"), authic, false, domain("host1.example"), ips("10.0.0.1"), nil)
test(ipdomain("cnameloop.example"), authic, authic, zerohost, nil, errCNAMELimit)
test(ipdomain("bogus.example"), authic, authic, zerohost, nil, &adns.DNSError{})
test(ipdomain("danglingcname.example"), authic, authic, zerohost, nil, &adns.DNSError{})
test(ipdomain("temperror-a.example"), authic, authic, zerohost, nil, &adns.DNSError{})
test(ipdomain("temperror-cname.example"), authic, authic, zerohost, nil, &adns.DNSError{})
test(ipdomain("host1.example"), authic, authic, zerohost, ips("10.0.0.1"), nil, "ip")
test(ipdomain("host1.example"), authic, authic, zerohost, ips("10.0.0.1"), nil, "ip4")
test(ipdomain("host1.example"), authic, authic, zerohost, nil, &adns.DNSError{}, "ip6")
test(ipdomain("host2.example"), authic, authic, zerohost, ips("10.0.0.2", "2001:db8::1"), nil, "ip")
test(ipdomain("host2.example"), authic, authic, zerohost, ips("10.0.0.2"), nil, "ip4")
test(ipdomain("host2.example"), authic, authic, zerohost, ips("2001:db8::1"), nil, "ip6")
test(ipdomain("cname-to-inauthentic.example"), authic, false, domain("host1.example"), ips("10.0.0.1"), nil, "ip")
test(ipdomain("cnameloop.example"), authic, authic, zerohost, nil, errCNAMELimit, "ip")
test(ipdomain("bogus.example"), authic, authic, zerohost, nil, &adns.DNSError{}, "ip")
test(ipdomain("danglingcname.example"), authic, authic, zerohost, nil, &adns.DNSError{}, "ip")
test(ipdomain("temperror-a.example"), authic, authic, zerohost, nil, &adns.DNSError{}, "ip")
test(ipdomain("temperror-cname.example"), authic, authic, zerohost, nil, &adns.DNSError{}, "ip")

}
test(ipdomain("cnameinauthentic.example"), false, false, domain("host1.example"), ips("10.0.0.1"), nil)
test(ipdomain("cname-to-inauthentic.example"), true, false, domain("host1.example"), ips("10.0.0.1"), nil)
test(ipdomain("cnameinauthentic.example"), false, false, domain("host1.example"), ips("10.0.0.1"), nil, "ip")
test(ipdomain("cname-to-inauthentic.example"), true, false, domain("host1.example"), ips("10.0.0.1"), nil, "ip")
}

func TestGatherTLSA(t *testing.T) {
Expand Down
Loading

0 comments on commit 73381d2

Please sign in to comment.