1
1
Fork 0
mirror of https://github.com/mjl-/mox.git synced 2025-02-13 23:42:28 +03:00

Enable configuration in IMAPS/Submissions sections

I think this is the smallest I can make this patch.
This commit is contained in:
s0ph0s 2024-12-07 01:13:32 -05:00
parent a183a9b174
commit 775c2f215e
6 changed files with 92 additions and 47 deletions

View file

@ -166,8 +166,9 @@ type Listener struct {
NoRequireSTARTTLS bool `sconf:"optional" sconf-doc:"Do not require STARTTLS. Since users must login, this means password may be sent without encryption. Not recommended."`
} `sconf:"optional" sconf-doc:"SMTP for submitting email, e.g. by email applications. Starts out in plain text, can be upgraded to TLS with the STARTTLS command. Prefer using Submissions which is always a TLS connection."`
Submissions struct {
Enabled bool
Port int `sconf:"optional" sconf-doc:"Default 465."`
Enabled bool
Port int `sconf:"optional" sconf-doc:"Default 465."`
EnableOnHTTPS bool `sconf:"optional" sconf-doc:"Additionally enable this Submissions listener on any active HTTPS listeners for port 443 via TLS ALPN. TLS Application Layer Protocol Negotiation allows clients to request a specific protocol from the server as part of the TLS connection setup. When this setting is enabled and a client requests the 'smtp' protocol after TLS, it will be able to talk SMTP to Mox on port 443. This is meant to be useful as a censorship circumvention technique for Delta Chat."`
} `sconf:"optional" sconf-doc:"SMTP over TLS for submitting email, by email applications. Requires a TLS config."`
IMAP struct {
Enabled bool
@ -175,8 +176,9 @@ type Listener struct {
NoRequireSTARTTLS bool `sconf:"optional" sconf-doc:"Enable this only when the connection is otherwise encrypted (e.g. through a VPN)."`
} `sconf:"optional" sconf-doc:"IMAP for reading email, by email applications. Starts out in plain text, can be upgraded to TLS with the STARTTLS command. Prefer using IMAPS instead which is always a TLS connection."`
IMAPS struct {
Enabled bool
Port int `sconf:"optional" sconf-doc:"Default 993."`
Enabled bool
Port int `sconf:"optional" sconf-doc:"Default 993."`
EnableOnHTTPS bool `sconf:"optional" sconf-doc:"Additionally enable this IMAP listener on any active HTTPS listeners for port 443 via TLS ALPN. TLS Application Layer Protocol Negotiation allows clients to request a specific protocol from the server as part of the TLS connection setup. When this setting is enabled and a client requests the 'imap' protocol after TLS, it will be able to talk IMAP to Mox on port 443. This is meant to be useful as a censorship circumvention technique for Delta Chat."`
} `sconf:"optional" sconf-doc:"IMAP over TLS for reading email, by email applications. Requires a TLS config."`
AccountHTTP WebService `sconf:"optional" sconf-doc:"Account web interface, for email users wanting to change their accounts, e.g. set new password, set new delivery rulesets. Default path is /."`
AccountHTTPS WebService `sconf:"optional" sconf-doc:"Account web interface listener like AccountHTTP, but for HTTPS. Requires a TLS config."`

View file

@ -283,6 +283,14 @@ See https://pkg.go.dev/github.com/mjl-/sconf for details.
# Default 465. (optional)
Port: 0
# Additionally enable this Submissions listener on any active HTTPS listeners for
# port 443 via TLS ALPN. TLS Application Layer Protocol Negotiation allows clients
# to request a specific protocol from the server as part of the TLS connection
# setup. When this setting is enabled and a client requests the 'smtp' protocol
# after TLS, it will be able to talk SMTP to Mox on port 443. This is meant to be
# useful as a censorship circumvention technique for Delta Chat. (optional)
EnableOnHTTPS: false
# IMAP for reading email, by email applications. Starts out in plain text, can be
# upgraded to TLS with the STARTTLS command. Prefer using IMAPS instead which is
# always a TLS connection. (optional)
@ -304,6 +312,14 @@ See https://pkg.go.dev/github.com/mjl-/sconf for details.
# Default 993. (optional)
Port: 0
# Additionally enable this IMAP listener on any active HTTPS listeners for port
# 443 via TLS ALPN. TLS Application Layer Protocol Negotiation allows clients to
# request a specific protocol from the server as part of the TLS connection setup.
# When this setting is enabled and a client requests the 'imap' protocol after
# TLS, it will be able to talk IMAP to Mox on port 443. This is meant to be useful
# as a censorship circumvention technique for Delta Chat. (optional)
EnableOnHTTPS: false
# Account web interface, for email users wanting to change their accounts, e.g.
# set new password, set new delivery rulesets. Default path is /. (optional)
AccountHTTP:

View file

@ -33,11 +33,9 @@ import (
"github.com/mjl-/mox/autotls"
"github.com/mjl-/mox/config"
"github.com/mjl-/mox/dns"
"github.com/mjl-/mox/imapserver"
"github.com/mjl-/mox/mlog"
"github.com/mjl-/mox/mox-"
"github.com/mjl-/mox/ratelimit"
"github.com/mjl-/mox/smtpserver"
"github.com/mjl-/mox/webaccount"
"github.com/mjl-/mox/webadmin"
"github.com/mjl-/mox/webapisrv"
@ -541,23 +539,35 @@ func redirectToTrailingSlash(srv *serve, hostMatch func(dns.IPDomain) bool, name
// Listen binds to sockets for HTTP listeners, including those required for ACME to
// generate TLS certificates. It stores the listeners so Serve can start serving them.
func Listen() {
func Listen(smtpHelper, imapHelper FnALPNHelper) {
// Initialize listeners in deterministic order for the same potential error
// messages.
names := maps.Keys(mox.Conf.Static.Listeners)
sort.Strings(names)
for _, name := range names {
found443 := false
l := mox.Conf.Static.Listeners[name]
portServe := portServes(l)
ports := maps.Keys(portServe)
sort.Ints(ports)
for _, port := range ports {
if port == 443 {
found443 = true
}
srv := portServe[port]
for _, ip := range l.IPs {
listen1(ip, port, srv.TLSConfig, name, srv.Kinds, srv)
listen1(ip, port, srv.TLSConfig, name, srv.Kinds, srv, smtpHelper, imapHelper)
}
}
if !found443 && (smtpHelper != nil || imapHelper != nil) {
pkglog.Warn(
"Listener asks for non-HTTP protocols to be available via ALPN on port 443, but does not configure any HTTPS listeners on port 443. Therefore, the EnableOnHTTPS setting has no effect. Configure an HTTPS listener on port 443 to fix this.",
slog.String("listenerName", name),
slog.Any("SubmissionsEnableOnHTTPS", smtpHelper != nil),
slog.Any("IMAPSEnableOnHTTPS", imapHelper != nil),
)
}
}
}
@ -780,9 +790,6 @@ func portServes(l config.Listener) map[int]*serve {
dc, ok := mox.Conf.Domain(dom)
return ok && !dc.ReportsOnly
}
// TODO: decide how to explicitly configure ALPN multiplexing in a way
// that makes sense, rather than tying it to Autoconfig.
srv.TLSConfig.NextProtos = []string{"h2", "imap", "smtp"}
srv.SystemHandle("autoconfig", autoconfigMatch, "/mail/config-v1.1.xml", mox.SafeHeaders(http.HandlerFunc(autoconfHandle)))
srv.SystemHandle("autodiscover", autoconfigMatch, "/autodiscover/autodiscover.xml", mox.SafeHeaders(http.HandlerFunc(autodiscoverHandle)))
srv.SystemHandle("mobileconfig", autoconfigMatch, "/profile.mobileconfig", mox.SafeHeaders(http.HandlerFunc(mobileconfigHandle)))
@ -886,13 +893,38 @@ var servers []func()
// the certificate to be given during the first https connection.
var ensureManagerHosts = map[*autotls.Manager]map[dns.Domain]struct{}{}
type FnALPNHelper = func(*tls.Config, net.Conn)
type tlsNextProtoMap = map[string]func(*http.Server, *tls.Conn, http.Handler)
func serverTlsSetup(port int, tlsConfig *tls.Config, smtpHelper, imapHelper FnALPNHelper) (*tls.Config, tlsNextProtoMap) {
cfg := tlsConfig.Clone()
npMap := tlsNextProtoMap{}
if port != 443 {
return cfg, npMap
}
doConfig := func(proto string, helperFunc FnALPNHelper) {
if helperFunc != nil {
cfg.NextProtos = append(cfg.NextProtos, proto)
npMap[proto] = func(_ *http.Server, conn *tls.Conn, _ http.Handler) {
helperFunc(cfg, conn)
}
pkglog.Print("Enabled ALPN listener", slog.String("proto", proto), slog.Any("NextProtos", cfg.NextProtos))
}
}
doConfig("smtp", smtpHelper)
doConfig("imap", imapHelper)
return cfg, npMap
}
// listen prepares a listener, and adds it to "servers", to be launched (if not running as root) through Serve.
func listen1(ip string, port int, tlsConfig *tls.Config, name string, kinds []string, handler http.Handler) {
func listen1(ip string, port int, tlsConfig *tls.Config, name string, kinds []string, handler http.Handler, smtpHelper, imapHelper FnALPNHelper) {
addr := net.JoinHostPort(ip, fmt.Sprintf("%d", port))
var protocol string
var ln net.Listener
var err error
var updatedTlsConfig *tls.Config
var npMap tlsNextProtoMap
if tlsConfig == nil {
protocol = "http"
if os.Getuid() == 0 {
@ -913,24 +945,22 @@ func listen1(ip string, port int, tlsConfig *tls.Config, name string, kinds []st
slog.String("kinds", strings.Join(kinds, ",")),
slog.String("address", addr))
}
updatedTlsConfig, npMap = serverTlsSetup(port, tlsConfig, smtpHelper, imapHelper)
ln, err = mox.Listen(mox.Network(ip), addr)
if err != nil {
pkglog.Fatalx("https: listen", err, slog.String("addr", addr))
}
ln = tls.NewListener(ln, tlsConfig)
ln = tls.NewListener(ln, updatedTlsConfig)
}
server := &http.Server{
Handler: handler,
// Clone because our multiple Server.Serve calls modify config concurrently leading to data race.
TLSConfig: tlsConfig.Clone(),
TLSConfig: updatedTlsConfig,
ReadHeaderTimeout: 30 * time.Second,
IdleTimeout: 65 * time.Second, // Chrome closes connections after 60 seconds, firefox after 115 seconds.
ErrorLog: golog.New(mlog.LogWriter(pkglog.With(slog.String("pkg", "net/http")), slog.LevelInfo, protocol+" error"), "", 0),
TLSNextProto: map[string]func(*http.Server, *tls.Conn, http.Handler){
"imap": handleIMAP,
"smtp": handleSMTP,
},
TLSNextProto: npMap,
}
// By default, the Go 1.6 and above http.Server includes support for HTTP2.
// However, HTTP2 is negotiated via ALPN. Because we are configuring
@ -947,21 +977,6 @@ func listen1(ip string, port int, tlsConfig *tls.Config, name string, kinds []st
servers = append(servers, serve)
}
func handleIMAP(server *http.Server, conn *tls.Conn, _ http.Handler) {
cid := mox.Cid()
imapserver.ServeConn("imaps-alpn", cid, server.TLSConfig, conn, true, false)
}
func handleSMTP(server *http.Server, conn *tls.Conn, _ http.Handler) {
cid := mox.Cid()
hostname := mox.Conf.Static.HostnameDomain
var maxMsgSize int64 = config.DefaultMaxMsgSize
resolver := dns.StrictResolver{Log: pkglog.Logger}
smtpserver.ServeConn("submissions-alpn", cid, hostname, server.TLSConfig, conn, resolver, true, true, maxMsgSize, true, true, true, nil, 0)
}
// Serve starts serving on the initialized listeners.
func Serve() {
loadStaticGzipCache(mox.DataDirPath("tmp/httpstaticcompresscache"), 512*1024*1024)

View file

@ -66,6 +66,7 @@ import (
"github.com/mjl-/bstore"
"github.com/mjl-/mox/config"
"github.com/mjl-/mox/http"
"github.com/mjl-/mox/message"
"github.com/mjl-/mox/metrics"
"github.com/mjl-/mox/mlog"
@ -312,7 +313,8 @@ func (c *conn) xsanity(err error, format string, args ...any) {
type msgseq uint32
// Listen initializes all imap listeners for the configuration, and stores them for Serve to start them.
func Listen() {
func Listen() http.FnALPNHelper {
var alpnHelper http.FnALPNHelper
names := maps.Keys(mox.Conf.Static.Listeners)
sort.Strings(names)
for _, name := range names {
@ -332,11 +334,19 @@ func Listen() {
if listener.IMAPS.Enabled {
port := config.Port(listener.IMAPS.Port, 993)
protocol := "imaps"
for _, ip := range listener.IPs {
listen1("imaps", name, ip, port, tlsConfig, true, false)
listen1(protocol, name, ip, port, tlsConfig, true, false)
}
if listener.IMAPS.EnableOnHTTPS && alpnHelper == nil {
alpnHelper = func(tc *tls.Config, conn net.Conn) {
metricIMAPConnection.WithLabelValues(protocol).Inc()
serve(name, mox.Cid(), tc, conn, true, false)
}
}
}
}
return alpnHelper
}
var servers []func()
@ -623,10 +633,6 @@ func (c *conn) xhighestModSeq(tx *bstore.Tx, mailboxID int64) store.ModSeq {
var cleanClose struct{} // Sentinel value for panic/recover indicating clean close of connection.
func ServeConn(listenerName string, cid int64, tlsConfig *tls.Config, nc net.Conn, xtls, noRequireSTARTTLS bool) {
serve(listenerName, cid, tlsConfig, nc, xtls, noRequireSTARTTLS)
}
func serve(listenerName string, cid int64, tlsConfig *tls.Config, nc net.Conn, xtls, noRequireSTARTTLS bool) {
var remoteIP net.IP
if a, ok := nc.RemoteAddr().(*net.TCPAddr); ok {

View file

@ -54,9 +54,9 @@ func shutdown(log mlog.Log) {
// start initializes all packages, starts all listeners and the switchboard
// goroutine, then returns.
func start(mtastsdbRefresher, sendDMARCReports, sendTLSReports, skipForkExec bool) error {
smtpserver.Listen()
imapserver.Listen()
http.Listen()
smtpALPNHelper := smtpserver.Listen()
imapALPNHelper := imapserver.Listen()
http.Listen(smtpALPNHelper, imapALPNHelper)
if !skipForkExec {
// If we were just launched as root, fork and exec as unprivileged user, handing

View file

@ -45,6 +45,7 @@ import (
"github.com/mjl-/mox/dmarcrpt"
"github.com/mjl-/mox/dns"
"github.com/mjl-/mox/dsn"
"github.com/mjl-/mox/http"
"github.com/mjl-/mox/iprev"
"github.com/mjl-/mox/message"
"github.com/mjl-/mox/metrics"
@ -184,7 +185,8 @@ func durationDefault(delay *time.Duration, def time.Duration) time.Duration {
// Listen initializes network listeners for incoming SMTP connection.
// The listeners are stored for a later call to Serve.
func Listen() {
func Listen() http.FnALPNHelper {
var alpnHelper http.FnALPNHelper
names := maps.Keys(mox.Conf.Static.Listeners)
sort.Strings(names)
for _, name := range names {
@ -235,8 +237,16 @@ func Listen() {
for _, ip := range listener.IPs {
listen1("submissions", name, ip, port, hostname, tlsConfig, true, true, maxMsgSize, true, true, true, nil, 0)
}
if listener.Submissions.EnableOnHTTPS && alpnHelper == nil {
alpnHelper = func(tc *tls.Config, conn net.Conn) {
log := mlog.New("smtp-alpn", nil)
resolver := dns.StrictResolver{Log: log.Logger}
serve(name, mox.Cid(), hostname, tc, conn, resolver, true, true, maxMsgSize, true, true, true, nil, 0)
}
}
}
}
return alpnHelper
}
var servers []func()
@ -576,10 +586,6 @@ func (c *conn) writelinef(format string, args ...any) {
var cleanClose struct{} // Sentinel value for panic/recover indicating clean close of connection.
func ServeConn(listenerName string, cid int64, hostname dns.Domain, tlsConfig *tls.Config, nc net.Conn, resolver dns.Resolver, submission, tls bool, maxMessageSize int64, requireTLSForAuth, requireTLSForDelivery, requireTLS bool, dnsBLs []dns.Domain, firstTimeSenderDelay time.Duration) {
serve(listenerName, cid, hostname, tlsConfig, nc, resolver, submission, tls, maxMessageSize, requireTLSForAuth, requireTLSForDelivery, requireTLS, dnsBLs, firstTimeSenderDelay)
}
func serve(listenerName string, cid int64, hostname dns.Domain, tlsConfig *tls.Config, nc net.Conn, resolver dns.Resolver, submission, tls bool, maxMessageSize int64, requireTLSForAuth, requireTLSForDelivery, requireTLS bool, dnsBLs []dns.Domain, firstTimeSenderDelay time.Duration) {
var localIP, remoteIP net.IP
if a, ok := nc.LocalAddr().(*net.TCPAddr); ok {