From be570d1c7d3de0ddacb011b6411a302d7f7e9f9e Mon Sep 17 00:00:00 2001 From: Laurent Meunier Date: Mon, 8 Apr 2024 21:50:30 +0200 Subject: [PATCH] add TransportDirect transport The `TransportDirect` transport allows to tweak outgoing SMTP connections to remote servers. Currently, it only allows to select network IP family (ipv4, ipv6 or both). For example, to disable ipv6 for all outgoing SMTP connections: - add these lines in mox.conf to create a new transport named "disableipv6": ``` Transports: disableipv6: Direct: DisableIpv6: true ``` - then add these lines in domains.conf to use this transport: ``` Routes: - Transport: disableipv6 ``` fix #149 --- config/config.go | 16 ++++++++++++---- config/doc.go | 12 ++++++++++++ main.go | 2 +- mox-/config.go | 17 +++++++++++++++++ queue/direct.go | 19 ++++++++++++++----- queue/queue.go | 2 +- queue/submit.go | 2 +- smtpclient/dial_test.go | 4 ++-- smtpclient/gather.go | 8 ++++---- smtpclient/gather_test.go | 30 +++++++++++++++++------------- webadmin/admin.js | 6 ++++-- webadmin/api.json | 28 ++++++++++++++++++++++++++++ webadmin/api.ts | 12 ++++++++++-- webmail/api.go | 2 +- 14 files changed, 124 insertions(+), 36 deletions(-) diff --git a/config/config.go b/config/config.go index 910be52..7ad566d 100644 --- a/config/config.go +++ b/config/config.go @@ -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 @@ -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.. 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."` diff --git a/config/doc.go b/config/doc.go index 9f8717a..61462a0 100644 --- a/config/doc.go +++ b/config/doc.go @@ -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 diff --git a/main.go b/main.go index fa5e392..63a8bb2 100644 --- a/main.go +++ b/main.go @@ -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 diff --git a/mox-/config.go b/mox-/config.go index 3bc9d78..9df65af 100644 --- a/mox-/config.go +++ b/mox-/config.go @@ -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 { @@ -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) } diff --git a/queue/direct.go b/queue/direct.go index fa1df5d..3a66065 100644 --- a/queue/direct.go +++ b/queue/direct.go @@ -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" @@ -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 @@ -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 { @@ -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} @@ -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 @@ -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)) @@ -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 } diff --git a/queue/queue.go b/queue/queue.go index 8cc34d1..3d89017 100644 --- a/queue/queue.go +++ b/queue/queue.go @@ -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) } } diff --git a/queue/submit.go b/queue/submit.go index e081cc9..25b1c5d 100644 --- a/queue/submit.go +++ b/queue/submit.go @@ -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) diff --git a/smtpclient/dial_test.go b/smtpclient/dial_test.go index 1d014e9..1b1784e 100644 --- a/smtpclient/dial_test.go +++ b/smtpclient/dial_test.go @@ -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) } @@ -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) } diff --git a/smtpclient/gather.go b/smtpclient/gather.go index 4035f82..d7cf923 100644 --- a/smtpclient/gather.go +++ b/smtpclient/gather.go @@ -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 { @@ -216,7 +216,7 @@ 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 { @@ -224,8 +224,8 @@ func GatherIPs(ctx context.Context, elog *slog.Logger, resolver dns.Resolver, ho } 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 diff --git a/smtpclient/gather_test.go b/smtpclient/gather_test.go index 37903a2..de0b241 100644 --- a/smtpclient/gather_test.go +++ b/smtpclient/gather_test.go @@ -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) @@ -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) { diff --git a/webadmin/admin.js b/webadmin/admin.js index 5e65a25..895628e 100644 --- a/webadmin/admin.js +++ b/webadmin/admin.js @@ -336,7 +336,7 @@ var api; SPFResult["SPFTemperror"] = "temperror"; SPFResult["SPFPermerror"] = "permerror"; })(SPFResult = api.SPFResult || (api.SPFResult = {})); - api.structTypes = { "AuthResults": true, "AutoconfCheckResult": true, "AutodiscoverCheckResult": true, "AutodiscoverSRV": true, "CheckResult": true, "ClientConfigs": true, "ClientConfigsEntry": true, "DANECheckResult": true, "DKIMAuthResult": true, "DKIMCheckResult": true, "DKIMRecord": true, "DMARCCheckResult": true, "DMARCRecord": true, "DMARCSummary": true, "DNSSECResult": true, "DateRange": true, "Directive": true, "Domain": true, "DomainFeedback": true, "Evaluation": true, "EvaluationStat": true, "Extension": true, "FailureDetails": true, "Filter": true, "HoldRule": true, "IPDomain": true, "IPRevCheckResult": true, "Identifiers": true, "MTASTSCheckResult": true, "MTASTSRecord": true, "MX": true, "MXCheckResult": true, "Modifier": true, "Msg": true, "Pair": true, "Policy": true, "PolicyEvaluated": true, "PolicyOverrideReason": true, "PolicyPublished": true, "PolicyRecord": true, "Record": true, "Report": true, "ReportMetadata": true, "ReportRecord": true, "Result": true, "ResultPolicy": true, "Reverse": true, "Row": true, "SMTPAuth": true, "SPFAuthResult": true, "SPFCheckResult": true, "SPFRecord": true, "SRV": true, "SRVConfCheckResult": true, "STSMX": true, "Summary": true, "SuppressAddress": true, "TLSCheckResult": true, "TLSRPTCheckResult": true, "TLSRPTDateRange": true, "TLSRPTRecord": true, "TLSRPTSummary": true, "TLSRPTSuppressAddress": true, "TLSReportRecord": true, "TLSResult": true, "Transport": true, "TransportSMTP": true, "TransportSocks": true, "URI": true, "WebForward": true, "WebHandler": true, "WebRedirect": true, "WebStatic": true, "WebserverConfig": true }; + api.structTypes = { "AuthResults": true, "AutoconfCheckResult": true, "AutodiscoverCheckResult": true, "AutodiscoverSRV": true, "CheckResult": true, "ClientConfigs": true, "ClientConfigsEntry": true, "DANECheckResult": true, "DKIMAuthResult": true, "DKIMCheckResult": true, "DKIMRecord": true, "DMARCCheckResult": true, "DMARCRecord": true, "DMARCSummary": true, "DNSSECResult": true, "DateRange": true, "Directive": true, "Domain": true, "DomainFeedback": true, "Evaluation": true, "EvaluationStat": true, "Extension": true, "FailureDetails": true, "Filter": true, "HoldRule": true, "IPDomain": true, "IPRevCheckResult": true, "Identifiers": true, "MTASTSCheckResult": true, "MTASTSRecord": true, "MX": true, "MXCheckResult": true, "Modifier": true, "Msg": true, "Pair": true, "Policy": true, "PolicyEvaluated": true, "PolicyOverrideReason": true, "PolicyPublished": true, "PolicyRecord": true, "Record": true, "Report": true, "ReportMetadata": true, "ReportRecord": true, "Result": true, "ResultPolicy": true, "Reverse": true, "Row": true, "SMTPAuth": true, "SPFAuthResult": true, "SPFCheckResult": true, "SPFRecord": true, "SRV": true, "SRVConfCheckResult": true, "STSMX": true, "Summary": true, "SuppressAddress": true, "TLSCheckResult": true, "TLSRPTCheckResult": true, "TLSRPTDateRange": true, "TLSRPTRecord": true, "TLSRPTSummary": true, "TLSRPTSuppressAddress": true, "TLSReportRecord": true, "TLSResult": true, "Transport": true, "TransportDirect": true, "TransportSMTP": true, "TransportSocks": true, "URI": true, "WebForward": true, "WebHandler": true, "WebRedirect": true, "WebStatic": true, "WebserverConfig": true }; api.stringsTypes = { "Align": true, "Alignment": true, "CSRFToken": true, "DKIMResult": true, "DMARCPolicy": true, "DMARCResult": true, "Disposition": true, "IP": true, "Localpart": true, "Mode": true, "PolicyOverride": true, "PolicyType": true, "RUA": true, "ResultType": true, "SPFDomainScope": true, "SPFResult": true }; api.intsTypes = {}; api.types = { @@ -405,10 +405,11 @@ var api; "WebStatic": { "Name": "WebStatic", "Docs": "", "Fields": [{ "Name": "StripPrefix", "Docs": "", "Typewords": ["string"] }, { "Name": "Root", "Docs": "", "Typewords": ["string"] }, { "Name": "ListFiles", "Docs": "", "Typewords": ["bool"] }, { "Name": "ContinueNotFound", "Docs": "", "Typewords": ["bool"] }, { "Name": "ResponseHeaders", "Docs": "", "Typewords": ["{}", "string"] }] }, "WebRedirect": { "Name": "WebRedirect", "Docs": "", "Fields": [{ "Name": "BaseURL", "Docs": "", "Typewords": ["string"] }, { "Name": "OrigPathRegexp", "Docs": "", "Typewords": ["string"] }, { "Name": "ReplacePath", "Docs": "", "Typewords": ["string"] }, { "Name": "StatusCode", "Docs": "", "Typewords": ["int32"] }] }, "WebForward": { "Name": "WebForward", "Docs": "", "Fields": [{ "Name": "StripPath", "Docs": "", "Typewords": ["bool"] }, { "Name": "URL", "Docs": "", "Typewords": ["string"] }, { "Name": "ResponseHeaders", "Docs": "", "Typewords": ["{}", "string"] }] }, - "Transport": { "Name": "Transport", "Docs": "", "Fields": [{ "Name": "Submissions", "Docs": "", "Typewords": ["nullable", "TransportSMTP"] }, { "Name": "Submission", "Docs": "", "Typewords": ["nullable", "TransportSMTP"] }, { "Name": "SMTP", "Docs": "", "Typewords": ["nullable", "TransportSMTP"] }, { "Name": "Socks", "Docs": "", "Typewords": ["nullable", "TransportSocks"] }] }, + "Transport": { "Name": "Transport", "Docs": "", "Fields": [{ "Name": "Submissions", "Docs": "", "Typewords": ["nullable", "TransportSMTP"] }, { "Name": "Submission", "Docs": "", "Typewords": ["nullable", "TransportSMTP"] }, { "Name": "SMTP", "Docs": "", "Typewords": ["nullable", "TransportSMTP"] }, { "Name": "Socks", "Docs": "", "Typewords": ["nullable", "TransportSocks"] }, { "Name": "Direct", "Docs": "", "Typewords": ["nullable", "TransportDirect"] }] }, "TransportSMTP": { "Name": "TransportSMTP", "Docs": "", "Fields": [{ "Name": "Host", "Docs": "", "Typewords": ["string"] }, { "Name": "Port", "Docs": "", "Typewords": ["int32"] }, { "Name": "STARTTLSInsecureSkipVerify", "Docs": "", "Typewords": ["bool"] }, { "Name": "NoSTARTTLS", "Docs": "", "Typewords": ["bool"] }, { "Name": "Auth", "Docs": "", "Typewords": ["nullable", "SMTPAuth"] }] }, "SMTPAuth": { "Name": "SMTPAuth", "Docs": "", "Fields": [{ "Name": "Username", "Docs": "", "Typewords": ["string"] }, { "Name": "Password", "Docs": "", "Typewords": ["string"] }, { "Name": "Mechanisms", "Docs": "", "Typewords": ["[]", "string"] }] }, "TransportSocks": { "Name": "TransportSocks", "Docs": "", "Fields": [{ "Name": "Address", "Docs": "", "Typewords": ["string"] }, { "Name": "RemoteIPs", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "RemoteHostname", "Docs": "", "Typewords": ["string"] }] }, + "TransportDirect": { "Name": "TransportDirect", "Docs": "", "Fields": [{ "Name": "DisableIPv4", "Docs": "", "Typewords": ["bool"] }, { "Name": "DisableIPv6", "Docs": "", "Typewords": ["bool"] }] }, "EvaluationStat": { "Name": "EvaluationStat", "Docs": "", "Fields": [{ "Name": "Domain", "Docs": "", "Typewords": ["Domain"] }, { "Name": "Dispositions", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Count", "Docs": "", "Typewords": ["int32"] }, { "Name": "SendReport", "Docs": "", "Typewords": ["bool"] }] }, "Evaluation": { "Name": "Evaluation", "Docs": "", "Fields": [{ "Name": "ID", "Docs": "", "Typewords": ["int64"] }, { "Name": "PolicyDomain", "Docs": "", "Typewords": ["string"] }, { "Name": "Evaluated", "Docs": "", "Typewords": ["timestamp"] }, { "Name": "Optional", "Docs": "", "Typewords": ["bool"] }, { "Name": "IntervalHours", "Docs": "", "Typewords": ["int32"] }, { "Name": "Addresses", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "PolicyPublished", "Docs": "", "Typewords": ["PolicyPublished"] }, { "Name": "SourceIP", "Docs": "", "Typewords": ["string"] }, { "Name": "Disposition", "Docs": "", "Typewords": ["Disposition"] }, { "Name": "AlignedDKIMPass", "Docs": "", "Typewords": ["bool"] }, { "Name": "AlignedSPFPass", "Docs": "", "Typewords": ["bool"] }, { "Name": "OverrideReasons", "Docs": "", "Typewords": ["[]", "PolicyOverrideReason"] }, { "Name": "EnvelopeTo", "Docs": "", "Typewords": ["string"] }, { "Name": "EnvelopeFrom", "Docs": "", "Typewords": ["string"] }, { "Name": "HeaderFrom", "Docs": "", "Typewords": ["string"] }, { "Name": "DKIMResults", "Docs": "", "Typewords": ["[]", "DKIMAuthResult"] }, { "Name": "SPFResults", "Docs": "", "Typewords": ["[]", "SPFAuthResult"] }] }, "SuppressAddress": { "Name": "SuppressAddress", "Docs": "", "Fields": [{ "Name": "ID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Inserted", "Docs": "", "Typewords": ["timestamp"] }, { "Name": "ReportingAddress", "Docs": "", "Typewords": ["string"] }, { "Name": "Until", "Docs": "", "Typewords": ["timestamp"] }, { "Name": "Comment", "Docs": "", "Typewords": ["string"] }] }, @@ -501,6 +502,7 @@ var api; TransportSMTP: (v) => api.parse("TransportSMTP", v), SMTPAuth: (v) => api.parse("SMTPAuth", v), TransportSocks: (v) => api.parse("TransportSocks", v), + TransportDirect: (v) => api.parse("TransportDirect", v), EvaluationStat: (v) => api.parse("EvaluationStat", v), Evaluation: (v) => api.parse("Evaluation", v), SuppressAddress: (v) => api.parse("SuppressAddress", v), diff --git a/webadmin/api.json b/webadmin/api.json index 765a843..e732bae 100644 --- a/webadmin/api.json +++ b/webadmin/api.json @@ -4135,6 +4135,14 @@ "nullable", "TransportSocks" ] + }, + { + "Name": "Direct", + "Docs": "", + "Typewords": [ + "nullable", + "TransportDirect" + ] } ] }, @@ -4236,6 +4244,26 @@ } ] }, + { + "Name": "TransportDirect", + "Docs": "", + "Fields": [ + { + "Name": "DisableIPv4", + "Docs": "", + "Typewords": [ + "bool" + ] + }, + { + "Name": "DisableIPv6", + "Docs": "", + "Typewords": [ + "bool" + ] + } + ] + }, { "Name": "EvaluationStat", "Docs": "EvaluationStat summarizes stored evaluations, for inclusion in an upcoming\naggregate report, for a domain.", diff --git a/webadmin/api.ts b/webadmin/api.ts index 0335a75..966db4a 100644 --- a/webadmin/api.ts +++ b/webadmin/api.ts @@ -584,6 +584,7 @@ export interface Transport { Submission?: TransportSMTP | null SMTP?: TransportSMTP | null Socks?: TransportSocks | null + Direct?: TransportDirect | null } // TransportSMTP delivers messages by "submission" (SMTP, typically @@ -611,6 +612,11 @@ export interface TransportSocks { RemoteHostname: string } +export interface TransportDirect { + DisableIPv4: boolean + DisableIPv6: boolean +} + // EvaluationStat summarizes stored evaluations, for inclusion in an upcoming // aggregate report, for a domain. export interface EvaluationStat { @@ -810,7 +816,7 @@ export type Localpart = string // be an IPv4 address. export type IP = string -export const structTypes: {[typename: string]: boolean} = {"AuthResults":true,"AutoconfCheckResult":true,"AutodiscoverCheckResult":true,"AutodiscoverSRV":true,"CheckResult":true,"ClientConfigs":true,"ClientConfigsEntry":true,"DANECheckResult":true,"DKIMAuthResult":true,"DKIMCheckResult":true,"DKIMRecord":true,"DMARCCheckResult":true,"DMARCRecord":true,"DMARCSummary":true,"DNSSECResult":true,"DateRange":true,"Directive":true,"Domain":true,"DomainFeedback":true,"Evaluation":true,"EvaluationStat":true,"Extension":true,"FailureDetails":true,"Filter":true,"HoldRule":true,"IPDomain":true,"IPRevCheckResult":true,"Identifiers":true,"MTASTSCheckResult":true,"MTASTSRecord":true,"MX":true,"MXCheckResult":true,"Modifier":true,"Msg":true,"Pair":true,"Policy":true,"PolicyEvaluated":true,"PolicyOverrideReason":true,"PolicyPublished":true,"PolicyRecord":true,"Record":true,"Report":true,"ReportMetadata":true,"ReportRecord":true,"Result":true,"ResultPolicy":true,"Reverse":true,"Row":true,"SMTPAuth":true,"SPFAuthResult":true,"SPFCheckResult":true,"SPFRecord":true,"SRV":true,"SRVConfCheckResult":true,"STSMX":true,"Summary":true,"SuppressAddress":true,"TLSCheckResult":true,"TLSRPTCheckResult":true,"TLSRPTDateRange":true,"TLSRPTRecord":true,"TLSRPTSummary":true,"TLSRPTSuppressAddress":true,"TLSReportRecord":true,"TLSResult":true,"Transport":true,"TransportSMTP":true,"TransportSocks":true,"URI":true,"WebForward":true,"WebHandler":true,"WebRedirect":true,"WebStatic":true,"WebserverConfig":true} +export const structTypes: {[typename: string]: boolean} = {"AuthResults":true,"AutoconfCheckResult":true,"AutodiscoverCheckResult":true,"AutodiscoverSRV":true,"CheckResult":true,"ClientConfigs":true,"ClientConfigsEntry":true,"DANECheckResult":true,"DKIMAuthResult":true,"DKIMCheckResult":true,"DKIMRecord":true,"DMARCCheckResult":true,"DMARCRecord":true,"DMARCSummary":true,"DNSSECResult":true,"DateRange":true,"Directive":true,"Domain":true,"DomainFeedback":true,"Evaluation":true,"EvaluationStat":true,"Extension":true,"FailureDetails":true,"Filter":true,"HoldRule":true,"IPDomain":true,"IPRevCheckResult":true,"Identifiers":true,"MTASTSCheckResult":true,"MTASTSRecord":true,"MX":true,"MXCheckResult":true,"Modifier":true,"Msg":true,"Pair":true,"Policy":true,"PolicyEvaluated":true,"PolicyOverrideReason":true,"PolicyPublished":true,"PolicyRecord":true,"Record":true,"Report":true,"ReportMetadata":true,"ReportRecord":true,"Result":true,"ResultPolicy":true,"Reverse":true,"Row":true,"SMTPAuth":true,"SPFAuthResult":true,"SPFCheckResult":true,"SPFRecord":true,"SRV":true,"SRVConfCheckResult":true,"STSMX":true,"Summary":true,"SuppressAddress":true,"TLSCheckResult":true,"TLSRPTCheckResult":true,"TLSRPTDateRange":true,"TLSRPTRecord":true,"TLSRPTSummary":true,"TLSRPTSuppressAddress":true,"TLSReportRecord":true,"TLSResult":true,"Transport":true,"TransportDirect":true,"TransportSMTP":true,"TransportSocks":true,"URI":true,"WebForward":true,"WebHandler":true,"WebRedirect":true,"WebStatic":true,"WebserverConfig":true} export const stringsTypes: {[typename: string]: boolean} = {"Align":true,"Alignment":true,"CSRFToken":true,"DKIMResult":true,"DMARCPolicy":true,"DMARCResult":true,"Disposition":true,"IP":true,"Localpart":true,"Mode":true,"PolicyOverride":true,"PolicyType":true,"RUA":true,"ResultType":true,"SPFDomainScope":true,"SPFResult":true} export const intsTypes: {[typename: string]: boolean} = {} export const types: TypenameMap = { @@ -879,10 +885,11 @@ export const types: TypenameMap = { "WebStatic": {"Name":"WebStatic","Docs":"","Fields":[{"Name":"StripPrefix","Docs":"","Typewords":["string"]},{"Name":"Root","Docs":"","Typewords":["string"]},{"Name":"ListFiles","Docs":"","Typewords":["bool"]},{"Name":"ContinueNotFound","Docs":"","Typewords":["bool"]},{"Name":"ResponseHeaders","Docs":"","Typewords":["{}","string"]}]}, "WebRedirect": {"Name":"WebRedirect","Docs":"","Fields":[{"Name":"BaseURL","Docs":"","Typewords":["string"]},{"Name":"OrigPathRegexp","Docs":"","Typewords":["string"]},{"Name":"ReplacePath","Docs":"","Typewords":["string"]},{"Name":"StatusCode","Docs":"","Typewords":["int32"]}]}, "WebForward": {"Name":"WebForward","Docs":"","Fields":[{"Name":"StripPath","Docs":"","Typewords":["bool"]},{"Name":"URL","Docs":"","Typewords":["string"]},{"Name":"ResponseHeaders","Docs":"","Typewords":["{}","string"]}]}, - "Transport": {"Name":"Transport","Docs":"","Fields":[{"Name":"Submissions","Docs":"","Typewords":["nullable","TransportSMTP"]},{"Name":"Submission","Docs":"","Typewords":["nullable","TransportSMTP"]},{"Name":"SMTP","Docs":"","Typewords":["nullable","TransportSMTP"]},{"Name":"Socks","Docs":"","Typewords":["nullable","TransportSocks"]}]}, + "Transport": {"Name":"Transport","Docs":"","Fields":[{"Name":"Submissions","Docs":"","Typewords":["nullable","TransportSMTP"]},{"Name":"Submission","Docs":"","Typewords":["nullable","TransportSMTP"]},{"Name":"SMTP","Docs":"","Typewords":["nullable","TransportSMTP"]},{"Name":"Socks","Docs":"","Typewords":["nullable","TransportSocks"]},{"Name":"Direct","Docs":"","Typewords":["nullable","TransportDirect"]}]}, "TransportSMTP": {"Name":"TransportSMTP","Docs":"","Fields":[{"Name":"Host","Docs":"","Typewords":["string"]},{"Name":"Port","Docs":"","Typewords":["int32"]},{"Name":"STARTTLSInsecureSkipVerify","Docs":"","Typewords":["bool"]},{"Name":"NoSTARTTLS","Docs":"","Typewords":["bool"]},{"Name":"Auth","Docs":"","Typewords":["nullable","SMTPAuth"]}]}, "SMTPAuth": {"Name":"SMTPAuth","Docs":"","Fields":[{"Name":"Username","Docs":"","Typewords":["string"]},{"Name":"Password","Docs":"","Typewords":["string"]},{"Name":"Mechanisms","Docs":"","Typewords":["[]","string"]}]}, "TransportSocks": {"Name":"TransportSocks","Docs":"","Fields":[{"Name":"Address","Docs":"","Typewords":["string"]},{"Name":"RemoteIPs","Docs":"","Typewords":["[]","string"]},{"Name":"RemoteHostname","Docs":"","Typewords":["string"]}]}, + "TransportDirect": {"Name":"TransportDirect","Docs":"","Fields":[{"Name":"DisableIPv4","Docs":"","Typewords":["bool"]},{"Name":"DisableIPv6","Docs":"","Typewords":["bool"]}]}, "EvaluationStat": {"Name":"EvaluationStat","Docs":"","Fields":[{"Name":"Domain","Docs":"","Typewords":["Domain"]},{"Name":"Dispositions","Docs":"","Typewords":["[]","string"]},{"Name":"Count","Docs":"","Typewords":["int32"]},{"Name":"SendReport","Docs":"","Typewords":["bool"]}]}, "Evaluation": {"Name":"Evaluation","Docs":"","Fields":[{"Name":"ID","Docs":"","Typewords":["int64"]},{"Name":"PolicyDomain","Docs":"","Typewords":["string"]},{"Name":"Evaluated","Docs":"","Typewords":["timestamp"]},{"Name":"Optional","Docs":"","Typewords":["bool"]},{"Name":"IntervalHours","Docs":"","Typewords":["int32"]},{"Name":"Addresses","Docs":"","Typewords":["[]","string"]},{"Name":"PolicyPublished","Docs":"","Typewords":["PolicyPublished"]},{"Name":"SourceIP","Docs":"","Typewords":["string"]},{"Name":"Disposition","Docs":"","Typewords":["Disposition"]},{"Name":"AlignedDKIMPass","Docs":"","Typewords":["bool"]},{"Name":"AlignedSPFPass","Docs":"","Typewords":["bool"]},{"Name":"OverrideReasons","Docs":"","Typewords":["[]","PolicyOverrideReason"]},{"Name":"EnvelopeTo","Docs":"","Typewords":["string"]},{"Name":"EnvelopeFrom","Docs":"","Typewords":["string"]},{"Name":"HeaderFrom","Docs":"","Typewords":["string"]},{"Name":"DKIMResults","Docs":"","Typewords":["[]","DKIMAuthResult"]},{"Name":"SPFResults","Docs":"","Typewords":["[]","SPFAuthResult"]}]}, "SuppressAddress": {"Name":"SuppressAddress","Docs":"","Fields":[{"Name":"ID","Docs":"","Typewords":["int64"]},{"Name":"Inserted","Docs":"","Typewords":["timestamp"]},{"Name":"ReportingAddress","Docs":"","Typewords":["string"]},{"Name":"Until","Docs":"","Typewords":["timestamp"]},{"Name":"Comment","Docs":"","Typewords":["string"]}]}, @@ -976,6 +983,7 @@ export const parser = { TransportSMTP: (v: any) => parse("TransportSMTP", v) as TransportSMTP, SMTPAuth: (v: any) => parse("SMTPAuth", v) as SMTPAuth, TransportSocks: (v: any) => parse("TransportSocks", v) as TransportSocks, + TransportDirect: (v: any) => parse("TransportDirect", v) as TransportDirect, EvaluationStat: (v: any) => parse("EvaluationStat", v) as EvaluationStat, Evaluation: (v: any) => parse("Evaluation", v) as Evaluation, SuppressAddress: (v: any) => parse("SuppressAddress", v) as SuppressAddress, diff --git a/webmail/api.go b/webmail/api.go index 82c54af..10fd0a7 100644 --- a/webmail/api.go +++ b/webmail/api.go @@ -1732,7 +1732,7 @@ func recipientSecurity(ctx context.Context, resolver dns.Resolver, messageAddres // Resolve the IPs. Required for DANE to prevent bad DNS servers from causing an // error result instead of no-DANE result. - authentic, expandedAuthentic, expandedHost, _, _, err := smtpclient.GatherIPs(ctx, log.Logger, resolver, host, map[string][]net.IP{}) + authentic, expandedAuthentic, expandedHost, _, _, err := smtpclient.GatherIPs(ctx, log.Logger, resolver, "ip", host, map[string][]net.IP{}) if err != nil { rs.DANE = SecurityResultError return