Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for negotiating IMAP, SMTP & HTTP on 443 #255

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,17 +168,19 @@ 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
Port int `sconf:"optional" sconf-doc:"Default 143."`
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."`
Expand Down
16 changes: 16 additions & 0 deletions config/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,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)
Expand All @@ -308,6 +316,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:
Expand Down
10 changes: 5 additions & 5 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ require (
github.com/prometheus/client_golang v1.18.0
github.com/russross/blackfriday/v2 v2.1.0
go.etcd.io/bbolt v1.3.11
golang.org/x/crypto v0.27.0
golang.org/x/crypto v0.29.0
golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f
golang.org/x/net v0.29.0
golang.org/x/text v0.18.0
golang.org/x/net v0.31.0
golang.org/x/text v0.20.0
rsc.io/qr v0.2.0
)

Expand All @@ -30,8 +30,8 @@ require (
github.com/prometheus/common v0.45.0 // indirect
github.com/prometheus/procfs v0.12.0 // indirect
golang.org/x/mod v0.21.0 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/sys v0.25.0 // indirect
golang.org/x/sync v0.9.0 // indirect
golang.org/x/sys v0.27.0 // indirect
golang.org/x/tools v0.25.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect
)
10 changes: 10 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f h1:99ci1mjWVBWwJiEKYY6jWa4d2nTQVIEhZIptnrVb1XY=
golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI=
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
Expand All @@ -85,20 +87,28 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE=
golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg=
Expand Down
58 changes: 53 additions & 5 deletions http/web.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
_ "net/http/pprof"

"golang.org/x/exp/maps"
"golang.org/x/net/http2"

"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
Expand Down Expand Up @@ -538,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("listener", name),
slog.Any("submissionhttps", smtpHelper != nil),
slog.Any("imaphttps", imapHelper != nil),
)
}
}
}

Expand Down Expand Up @@ -886,13 +899,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 {
Expand All @@ -913,20 +951,30 @@ 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: 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
// TLSNextProto above, we have to explicitly enable HTTP2 by importing http2
// and calling ConfigureServer.
err = http2.ConfigureServer(server, nil)
if err != nil {
pkglog.Fatalx("https: unable to configure http2", err)
}
serve := func() {
err := server.Serve(ln)
Expand Down
23 changes: 18 additions & 5 deletions imapserver/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,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"
Expand Down Expand Up @@ -169,6 +170,7 @@ type conn struct {
state state
conn net.Conn
tls bool // Whether TLS has been initialized.
viaHTTPS bool // Whether this connection came in via HTTPS (using TLS ALPN).
br *bufio.Reader // From remote, with TLS unwrapped in case of TLS.
line chan lineErr // If set, instead of reading from br, a line is read from this channel. For reading a line in IDLE while also waiting for mailbox/account updates.
lastLine string // For detecting if syntax error is fatal, i.e. if this ends with a literal. Without crlf.
Expand Down Expand Up @@ -318,7 +320,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 {
Expand All @@ -338,11 +341,20 @@ 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) {
protocol = protocol + "https"
metricIMAPConnection.WithLabelValues(protocol).Inc()
serve(name, mox.Cid(), tc, conn, true, false, true)
}
}
}
}
return alpnHelper
}

var servers []func()
Expand Down Expand Up @@ -380,7 +392,7 @@ func listen1(protocol, listenerName, ip string, port int, tlsConfig *tls.Config,
}

metricIMAPConnection.WithLabelValues(protocol).Inc()
go serve(listenerName, mox.Cid(), tlsConfig, conn, xtls, noRequireSTARTTLS)
go serve(listenerName, mox.Cid(), tlsConfig, conn, xtls, noRequireSTARTTLS, false)
}
}

Expand Down Expand Up @@ -635,7 +647,7 @@ 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 serve(listenerName string, cid int64, tlsConfig *tls.Config, nc net.Conn, xtls, noRequireSTARTTLS bool) {
func serve(listenerName string, cid int64, tlsConfig *tls.Config, nc net.Conn, xtls, noRequireSTARTTLS, viaHTTPS bool) {
var remoteIP net.IP
if a, ok := nc.RemoteAddr().(*net.TCPAddr); ok {
remoteIP = a.IP
Expand All @@ -648,6 +660,7 @@ func serve(listenerName string, cid int64, tlsConfig *tls.Config, nc net.Conn, x
cid: cid,
conn: nc,
tls: xtls,
viaHTTPS: viaHTTPS,
lastlog: time.Now(),
baseTLSConfig: tlsConfig,
remoteIP: remoteIP,
Expand Down Expand Up @@ -717,7 +730,7 @@ func serve(listenerName string, cid int64, tlsConfig *tls.Config, nc net.Conn, x
}
}()

if xtls {
if xtls && !viaHTTPS {
// Start TLS on connection. We perform the handshake explicitly, so we can set a
// timeout, do client certificate authentication, log TLS details afterwards.
c.xtlsHandshakeAndAuthenticate(c.conn)
Expand Down
6 changes: 3 additions & 3 deletions serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading