in quickstart, add -hostname flag and check public ips with 2 dnsbl's

- if the guessed hostname is not correct, you can specify one yourself. useful
  if you generate a config locally and deploy to a different machine.
- if explicit public ips are found, check them with spamhaus and spamcop DNSBLs
  and warn if they are listed, with links to check more DNSBLs. should prevent
  disappointment later on.
This commit is contained in:
Mechiel Lukkien 2023-03-05 15:40:26 +01:00
parent ce54c6f1db
commit 845a72d07a
No known key found for this signature in database
5 changed files with 243 additions and 139 deletions

View file

@ -88,7 +88,7 @@ type ACME struct {
} }
type Listener struct { type Listener struct {
IPs []string `sconf-doc:"Use 0.0.0.0 to listen on all IPv4 and/or :: to listen on all IPv6 addresses."` IPs []string `sconf-doc:"Use 0.0.0.0 to listen on all IPv4 and/or :: to listen on all IPv6 addresses, but it is better to explicitly specify the IPs you want to use for email, as mox will make sure outgoing connections will only be made from one of those IPs."`
Hostname string `sconf:"optional" sconf-doc:"If empty, the config global Hostname is used."` Hostname string `sconf:"optional" sconf-doc:"If empty, the config global Hostname is used."`
HostnameDomain dns.Domain `sconf:"-" json:"-"` // Set when parsing config. HostnameDomain dns.Domain `sconf:"-" json:"-"` // Set when parsing config.

View file

@ -93,7 +93,9 @@ describe-static" and "mox config describe-domains":
Listeners: Listeners:
x: x:
# Use 0.0.0.0 to listen on all IPv4 and/or :: to listen on all IPv6 addresses. # Use 0.0.0.0 to listen on all IPv4 and/or :: to listen on all IPv6 addresses, but
# it is better to explicitly specify the IPs you want to use for email, as mox
# will make sure outgoing connections will only be made from one of those IPs.
IPs: IPs:
- -

14
doc.go
View file

@ -14,7 +14,7 @@ low-maintenance self-hosted email.
mox [-config config/mox.conf] ... mox [-config config/mox.conf] ...
mox serve mox serve
mox quickstart [-existing-webserver] user@domain [user | uid] mox quickstart [-existing-webserver] [-hostname host] user@domain [user | uid]
mox stop mox stop
mox setaccountpassword address mox setaccountpassword address
mox setadminpassword mox setadminpassword
@ -91,6 +91,14 @@ systemd service file and prints commands to enable and start mox as service.
The user or uid is optional, defaults to "mox", and is the user or uid/gid mox The user or uid is optional, defaults to "mox", and is the user or uid/gid mox
will run as after initialization. will run as after initialization.
Quickstart assumes mox will run on the machine you run quickstart on and uses
its host name and public IPs. On many systems the hostname is not a fully
qualified domain name, but only the first dns "label", e.g. "mail" in case of
"mail.example.org". If so, quickstart does a reverse DNS lookup to find the
hostname, and as fallback uses the label plus the domain of the email address
you specified. Use flag -hostname to explicitly specify the hostname mox will
run on.
Mox is by far easiest to operate if you let it listen on port 443 (HTTPS) and Mox is by far easiest to operate if you let it listen on port 443 (HTTPS) and
80 (HTTP). TLS will be fully automatic with ACME with Let's Encrypt. 80 (HTTP). TLS will be fully automatic with ACME with Let's Encrypt.
@ -107,9 +115,11 @@ traffic to your existing backend applications. Look for "WebHandlers:" in the
output of "mox config describe-domains" and see the output of "mox example output of "mox config describe-domains" and see the output of "mox example
webhandlers". webhandlers".
usage: mox quickstart [-existing-webserver] user@domain [user | uid] usage: mox quickstart [-existing-webserver] [-hostname host] user@domain [user | uid]
-existing-webserver -existing-webserver
use if a webserver is already running, so mox won't listen on port 80 and 443; you'll have to provide tls certificates/keys, and configure the existing webserver as reverse proxy, forwarding requests to mox. use if a webserver is already running, so mox won't listen on port 80 and 443; you'll have to provide tls certificates/keys, and configure the existing webserver as reverse proxy, forwarding requests to mox.
-hostname string
hostname mox will run on, by default the hostname of the machine quickstart runs on; if specified, the IPs for the hostname are configured for the public listener
# mox stop # mox stop

View file

@ -4,6 +4,9 @@
# mkdir config data web # mkdir config data web
# docker-compose run mox mox quickstart you@yourdomain.example $(id -u mox) # docker-compose run mox mox quickstart you@yourdomain.example $(id -u mox)
# #
# note: if you are running quickstart on a different machine than you will deploy
# mox to, use the "quickstart -hostname ..." flag.
#
# After following the quickstart instructions you can start mox: # After following the quickstart instructions you can start mox:
# #
# docker-compose up # docker-compose up

View file

@ -7,6 +7,7 @@ import (
"fmt" "fmt"
"log" "log"
"net" "net"
"net/url"
"os" "os"
"path/filepath" "path/filepath"
"runtime" "runtime"
@ -22,6 +23,7 @@ import (
"github.com/mjl-/mox/config" "github.com/mjl-/mox/config"
"github.com/mjl-/mox/dns" "github.com/mjl-/mox/dns"
"github.com/mjl-/mox/dnsbl"
"github.com/mjl-/mox/mox-" "github.com/mjl-/mox/mox-"
"github.com/mjl-/mox/smtp" "github.com/mjl-/mox/smtp"
"github.com/mjl-/mox/store" "github.com/mjl-/mox/store"
@ -41,7 +43,7 @@ func pwgen() string {
} }
func cmdQuickstart(c *cmd) { func cmdQuickstart(c *cmd) {
c.params = "[-existing-webserver] user@domain [user | uid]" c.params = "[-existing-webserver] [-hostname host] user@domain [user | uid]"
c.help = `Quickstart generates configuration files and prints instructions to quickly set up a mox instance. c.help = `Quickstart generates configuration files and prints instructions to quickly set up a mox instance.
Quickstart writes configuration files, prints initial admin and account Quickstart writes configuration files, prints initial admin and account
@ -51,6 +53,14 @@ systemd service file and prints commands to enable and start mox as service.
The user or uid is optional, defaults to "mox", and is the user or uid/gid mox The user or uid is optional, defaults to "mox", and is the user or uid/gid mox
will run as after initialization. will run as after initialization.
Quickstart assumes mox will run on the machine you run quickstart on and uses
its host name and public IPs. On many systems the hostname is not a fully
qualified domain name, but only the first dns "label", e.g. "mail" in case of
"mail.example.org". If so, quickstart does a reverse DNS lookup to find the
hostname, and as fallback uses the label plus the domain of the email address
you specified. Use flag -hostname to explicitly specify the hostname mox will
run on.
Mox is by far easiest to operate if you let it listen on port 443 (HTTPS) and Mox is by far easiest to operate if you let it listen on port 443 (HTTPS) and
80 (HTTP). TLS will be fully automatic with ACME with Let's Encrypt. 80 (HTTP). TLS will be fully automatic with ACME with Let's Encrypt.
@ -68,7 +78,9 @@ output of "mox config describe-domains" and see the output of "mox example
webhandlers". webhandlers".
` `
var existingWebserver bool var existingWebserver bool
var hostname string
c.flag.BoolVar(&existingWebserver, "existing-webserver", false, "use if a webserver is already running, so mox won't listen on port 80 and 443; you'll have to provide tls certificates/keys, and configure the existing webserver as reverse proxy, forwarding requests to mox.") c.flag.BoolVar(&existingWebserver, "existing-webserver", false, "use if a webserver is already running, so mox won't listen on port 80 and 443; you'll have to provide tls certificates/keys, and configure the existing webserver as reverse proxy, forwarding requests to mox.")
c.flag.StringVar(&hostname, "hostname", "", "hostname mox will run on, by default the hostname of the machine quickstart runs on; if specified, the IPs for the hostname are configured for the public listener")
args := c.Parse() args := c.Parse()
if len(args) != 1 && len(args) != 2 { if len(args) != 1 && len(args) != 2 {
c.Usage() c.Usage()
@ -127,6 +139,25 @@ logging in with IMAP.
} }
} }
resolver := dns.StrictResolver{}
// We don't want to spend too much total time on the DNS lookups. Because DNS may
// not work during quickstart, and we don't want to loop doing requests and having
// to wait for a timeout each time.
resolveCtx, resolveCancel := context.WithTimeout(context.Background(), 10*time.Second)
defer resolveCancel()
// We are going to find the (public) IPs to listen on and possibly the host name.
// Start with reasonable defaults. We'll replace them specific IPs, if we can find them.
publicListenerIPs := []string{"0.0.0.0", "::"}
privateListenerIPs := []string{"127.0.0.1", "::1"}
// If we find IPs based on network interfaces, {public,private}ListenerIPs are set
// based on these values.
var privateIPs, publicIPs []string
var dnshostname dns.Domain
if hostname == "" {
// Gather IP addresses for public and private listeners. // Gather IP addresses for public and private listeners.
// If we cannot find addresses for a category we fallback to all ips or localhost ips. // If we cannot find addresses for a category we fallback to all ips or localhost ips.
// We look at each network interface. If an interface has a private address, we // We look at each network interface. If an interface has a private address, we
@ -135,7 +166,6 @@ logging in with IMAP.
if err != nil { if err != nil {
fatalf("listing network interfaces: %s", err) fatalf("listing network interfaces: %s", err)
} }
var privateIPs, publicIPs []string
parseAddrIP := func(s string) net.IP { parseAddrIP := func(s string) net.IP {
if strings.HasPrefix(s, "[") && strings.HasSuffix(s, "]") { if strings.HasPrefix(s, "[") && strings.HasSuffix(s, "]") {
s = s[1 : len(s)-1] s = s[1 : len(s)-1]
@ -184,8 +214,6 @@ logging in with IMAP.
} }
} }
publicListenerIPs := []string{"0.0.0.0", "::"}
privateListenerIPs := []string{"127.0.0.1", "::1"}
if len(publicIPs) > 0 { if len(publicIPs) > 0 {
publicListenerIPs = publicIPs publicListenerIPs = publicIPs
} }
@ -193,15 +221,12 @@ logging in with IMAP.
privateListenerIPs = privateIPs privateListenerIPs = privateIPs
} }
resolver := dns.StrictResolver{}
var hostname dns.Domain
hostnameStr, err := os.Hostname() hostnameStr, err := os.Hostname()
if err != nil { if err != nil {
fatalf("hostname: %s", err) fatalf("hostname: %s", err)
} }
if strings.Contains(hostnameStr, ".") { if strings.Contains(hostnameStr, ".") {
hostname, err = dns.ParseDomain(hostnameStr) dnshostname, err = dns.ParseDomain(hostnameStr)
if err != nil { if err != nil {
fatalf("parsing hostname: %v", err) fatalf("parsing hostname: %v", err)
} }
@ -219,9 +244,9 @@ logging in with IMAP.
fmt.Printf("\n%s", fmt.Sprintf(format, args...)) fmt.Printf("\n%s", fmt.Sprintf(format, args...))
} }
for _, ip := range publicIPs { for _, ip := range publicIPs {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) revctx, revcancel := context.WithTimeout(resolveCtx, 5*time.Second)
defer cancel() defer revcancel()
l, err := resolver.LookupAddr(ctx, ip) l, err := resolver.LookupAddr(revctx, ip)
if err != nil { if err != nil {
warnf("WARNING: looking up reverse name(s) for %s: %v", ip, err) warnf("WARNING: looking up reverse name(s) for %s: %v", ip, err)
} }
@ -239,23 +264,26 @@ logging in with IMAP.
return nameList[i] < nameList[j] return nameList[i] < nameList[j]
}) })
if len(nameList) == 0 { if len(nameList) == 0 {
hostname, err = dns.ParseDomain(hostnameStr + "." + domain.Name()) dnshostname, err = dns.ParseDomain(hostnameStr + "." + domain.Name())
if err != nil { if err != nil {
fmt.Println() fmt.Println()
fatalf("parsing hostname: %v", err) fatalf("parsing hostname: %v", err)
} }
warnf(`WARNING: cannot determine hostname because the system name is not an FQDN and warnf(`WARNING: cannot determine hostname because the system name is not an FQDN and
no public IPs resolving to an FQDN were found. Quickstart will continue with the no public IPs resolving to an FQDN were found. Quickstart guessed the host name
following hostname, please replace it in the suggested DNS records and config below. If it is not correct, please remove the generated config files and run
files if this is not correct: quickstart again with the -hostname flag.
%s %s
`, hostname) `, dnshostname)
} else { } else {
if len(nameList) > 1 { if len(nameList) > 1 {
warnf("WARNING: multiple hostnames found for the public IPs, using the first of: %s", strings.Join(nameList, ", ")) warnf(`WARNING: multiple hostnames found for the public IPs, using the first of: %s
If this is not correct, remove the generated config files and run quickstart
again with the -hostname flag.
`, strings.Join(nameList, ", "))
} }
hostname, err = dns.ParseDomain(nameList[0]) dnshostname, err = dns.ParseDomain(nameList[0])
if err != nil { if err != nil {
fmt.Println() fmt.Println()
fatalf("parsing hostname %s: %v", nameList[0], err) fatalf("parsing hostname %s: %v", nameList[0], err)
@ -264,16 +292,27 @@ files if this is not correct:
if warned { if warned {
fmt.Printf("\n\n") fmt.Printf("\n\n")
} else { } else {
fmt.Printf(" found %s\n", hostname) fmt.Printf(" found %s\n", dnshostname)
}
}
} else {
// Host name was explicitly configured on command-line. We'll try to use its public
// IPs below.
var err error
dnshostname, err = dns.ParseDomain(hostname)
if err != nil {
fatalf("parsing hostname: %v", err)
} }
} }
// todo: lookup without going through /etc/hosts, because a machine typically has its name configured there, and LookupIPAddr will return it, but we care about DNS settings that the rest of the world uses to find us. perhaps we should check if the address resolves to 127.0.0.0/8? // todo: lookup without going through /etc/hosts, because a machine typically has its name configured there, and LookupIPAddr will return it, but we care about DNS settings that the rest of the world uses to find us. perhaps we should check if the address resolves to 127.0.0.0/8?
fmt.Printf("Looking up IPs for hostname %s...", hostname) fmt.Printf("Looking up IPs for hostname %s...", dnshostname)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) ipctx, ipcancel := context.WithTimeout(resolveCtx, 5*time.Second)
defer cancel() defer ipcancel()
ips, err := resolver.LookupIPAddr(ctx, hostname.ASCII+".") ips, err := resolver.LookupIPAddr(ipctx, dnshostname.ASCII+".")
ipcancel()
var xips []net.IPAddr var xips []net.IPAddr
var xipstrs []string
for _, ip := range ips { for _, ip := range ips {
// During linux install, you may get an alias for you full hostname in /etc/hosts // During linux install, you may get an alias for you full hostname in /etc/hosts
// resolving to 127.0.1.1, which would result in a false positive about the // resolving to 127.0.1.1, which would result in a false positive about the
@ -281,12 +320,18 @@ files if this is not correct:
// otherwise know their FQDN. // otherwise know their FQDN.
if !ip.IP.IsLoopback() { if !ip.IP.IsLoopback() {
xips = append(xips, ip) xips = append(xips, ip)
xipstrs = append(xipstrs, ip.String())
} }
} }
if err == nil && len(xips) == 0 { if err == nil && len(xips) == 0 {
err = errors.New("hostname not in dns, probably only in /etc/hosts") err = errors.New("hostname not in dns, probably only in /etc/hosts")
} }
ips = xips ips = xips
if hostname != "" {
// Host name was specified, assume we will run on a machine with those IPs.
publicListenerIPs = xipstrs
publicIPs = xipstrs
}
if err != nil { if err != nil {
fmt.Printf(` fmt.Printf(`
@ -305,7 +350,7 @@ This likely means one of two things:
your public IPs resolve back (reverse) to your hostname. your public IPs resolve back (reverse) to your hostname.
`, hostname, err) `, dnshostname, err)
} else { } else {
fmt.Printf(" OK\n") fmt.Printf(" OK\n")
@ -320,7 +365,9 @@ This likely means one of two things:
s := ip.String() s := ip.String()
l = append(l, s) l = append(l, s)
go func() { go func() {
addrs, err := resolver.LookupAddr(ctx, s) revctx, revcancel := context.WithTimeout(resolveCtx, 5*time.Second)
defer revcancel()
addrs, err := resolver.LookupAddr(revctx, s)
results <- result{s, addrs, err} results <- result{s, addrs, err}
}() }()
} }
@ -347,21 +394,61 @@ This likely means one of two things:
if err != nil { if err != nil {
warnf("parsing reverse name %q for %s: %v", a, r.IP, err) warnf("parsing reverse name %q for %s: %v", a, r.IP, err)
} }
if d == hostname { if d == dnshostname {
match = true match = true
} }
} }
if !match { if !match {
warnf("reverse name(s) %s for ip %s do not match hostname %s, which will cause other mail servers to reject incoming messages from this IP", strings.Join(r.Addrs, ","), r.IP, hostname) warnf("reverse name(s) %s for ip %s do not match hostname %s, which will cause other mail servers to reject incoming messages from this IP", strings.Join(r.Addrs, ","), r.IP, dnshostname)
} }
} }
if warned { if warned {
fmt.Printf("\n\n\n") fmt.Printf("\n\n")
} else { } else {
fmt.Printf(" OK\n\n") fmt.Printf(" OK\n")
} }
} }
cancel()
zones := []dns.Domain{
{ASCII: "sbl.spamhaus.org"},
{ASCII: "bl.spamcop.net"},
}
if len(publicIPs) > 0 {
fmt.Printf("Checking whether your public IPs are listed in popular DNS block lists...")
var listed bool
for _, zone := range zones {
for _, ip := range publicIPs {
dnsblctx, dnsblcancel := context.WithTimeout(resolveCtx, 5*time.Second)
status, expl, err := dnsbl.Lookup(dnsblctx, resolver, zone, net.ParseIP(ip))
dnsblcancel()
if status == dnsbl.StatusPass {
continue
}
errstr := ""
if err != nil {
errstr = fmt.Sprintf(" (%s)", err)
}
fmt.Printf("\nWARNING: checking your public IP %s in DNS block list %s: %v %s%s", ip, zone.Name(), status, expl, errstr)
listed = true
}
}
if listed {
log.Printf(`
Other mail servers are likely to reject email from IPs that are in a blocklist.
If all your IPs are in block lists, you will encounter problems delivering
email. Your IP may be in block lists only temporarily. To see if your IPs are
listed in more DNS block lists, visit:
`)
for _, ip := range publicIPs {
fmt.Printf("- https://multirbl.valli.org/lookup/%s.html\n", url.PathEscape(ip))
}
fmt.Printf("\n")
} else {
fmt.Printf(" OK\n")
}
}
fmt.Printf("\n")
user := "mox" user := "mox"
if len(args) == 2 { if len(args) == 2 {
@ -373,7 +460,7 @@ This likely means one of two things:
DataDir: "../data", DataDir: "../data",
User: user, User: user,
LogLevel: "debug", // Help new users, they'll bring it back to info when it all works. LogLevel: "debug", // Help new users, they'll bring it back to info when it all works.
Hostname: hostname.Name(), Hostname: dnshostname.Name(),
AdminPasswordFile: "adminpasswd", AdminPasswordFile: "adminpasswd",
} }
if !existingWebserver { if !existingWebserver {
@ -402,7 +489,7 @@ This likely means one of two things:
public.IMAPS.Enabled = true public.IMAPS.Enabled = true
if existingWebserver { if existingWebserver {
hostbase := fmt.Sprintf("path/to/%s", hostname.Name()) hostbase := fmt.Sprintf("path/to/%s", dnshostname.Name())
mtastsbase := fmt.Sprintf("path/to/mta-sts.%s", domain.Name()) mtastsbase := fmt.Sprintf("path/to/mta-sts.%s", domain.Name())
autoconfigbase := fmt.Sprintf("path/to/autoconfig.%s", domain.Name()) autoconfigbase := fmt.Sprintf("path/to/autoconfig.%s", domain.Name())
public.TLS = &config.TLS{ public.TLS = &config.TLS{
@ -423,7 +510,9 @@ This likely means one of two things:
} }
// Suggest blocklists, but we'll comment them out after generating the config. // Suggest blocklists, but we'll comment them out after generating the config.
public.SMTP.DNSBLs = []string{"sbl.spamhaus.org", "bl.spamcop.net"} for _, zone := range zones {
public.SMTP.DNSBLs = append(public.SMTP.DNSBLs, zone.Name())
}
internal := config.Listener{ internal := config.Listener{
IPs: privateListenerIPs, IPs: privateListenerIPs,
@ -459,7 +548,7 @@ This likely means one of two things:
accountConf := mox.MakeAccountConfig(addr) accountConf := mox.MakeAccountConfig(addr)
const withMTASTS = true const withMTASTS = true
confDomain, keyPaths, err := mox.MakeDomainConfig(context.Background(), domain, hostname, username, withMTASTS) confDomain, keyPaths, err := mox.MakeDomainConfig(context.Background(), domain, dnshostname, username, withMTASTS)
if err != nil { if err != nil {
fatalf("making domain config: %s", err) fatalf("making domain config: %s", err)
} }
@ -592,7 +681,7 @@ The paths are relative to config/ directory that holds mox.conf! To test if your
config is valid, run: config is valid, run:
./mox config test ./mox config test
`, domain.ASCII, domain.ASCII, hostname.ASCII) `, domain.ASCII, domain.ASCII, dnshostname.ASCII)
} else { } else {
fmt.Printf(` fmt.Printf(`
Configuration files have been written to config/mox.conf and Configuration files have been written to config/mox.conf and