10 cryptorand "crypto/rand"
31 "golang.org/x/text/unicode/norm"
33 "github.com/mjl-/autocert"
35 "github.com/mjl-/sconf"
37 "github.com/mjl-/mox/autotls"
38 "github.com/mjl-/mox/config"
39 "github.com/mjl-/mox/dns"
40 "github.com/mjl-/mox/mlog"
41 "github.com/mjl-/mox/moxio"
42 "github.com/mjl-/mox/moxvar"
43 "github.com/mjl-/mox/mtasts"
44 "github.com/mjl-/mox/smtp"
47var xlog = mlog.New("mox")
49// Config paths are set early in program startup. They will point to files in
52 ConfigStaticPath string
53 ConfigDynamicPath string
54 Conf = Config{Log: map[string]mlog.Level{"": mlog.LevelError}}
57// Config as used in the code, a processed version of what is in the config file.
59// Use methods to lookup a domain/account/address in the dynamic configuration.
61 Static config.Static // Does not change during the lifetime of a running instance.
63 logMutex sync.Mutex // For accessing the log levels.
64 Log map[string]mlog.Level
66 dynamicMutex sync.Mutex
67 Dynamic config.Dynamic // Can only be accessed directly by tests. Use methods on Config for locked access.
68 dynamicMtime time.Time
69 DynamicLastCheck time.Time // For use by quickstart only to skip checks.
70 // From canonical full address (localpart@domain, lower-cased when
71 // case-insensitive, stripped of catchall separator) to account and address.
72 // Domains are IDNA names in utf8.
73 accountDestinations map[string]AccountDestination
76type AccountDestination struct {
77 Catchall bool // If catchall destination for its domain.
78 Localpart smtp.Localpart // In original casing as written in config file.
80 Destination config.Destination
83// LogLevelSet sets a new log level for pkg. An empty pkg sets the default log
84// value that is used if no explicit log level is configured for a package.
85// This change is ephemeral, no config file is changed.
86func (c *Config) LogLevelSet(pkg string, level mlog.Level) {
88 defer c.logMutex.Unlock()
89 l := c.copyLogLevels()
92 xlog.Print("log level changed", mlog.Field("pkg", pkg), mlog.Field("level", mlog.LevelStrings[level]))
96// LogLevelRemove removes a configured log level for a package.
97func (c *Config) LogLevelRemove(pkg string) {
99 defer c.logMutex.Unlock()
100 l := c.copyLogLevels()
103 xlog.Print("log level cleared", mlog.Field("pkg", pkg))
104 mlog.SetConfig(c.Log)
107// copyLogLevels returns a copy of c.Log, for modifications.
108// must be called with log lock held.
109func (c *Config) copyLogLevels() map[string]mlog.Level {
110 m := map[string]mlog.Level{}
111 for pkg, level := range c.Log {
117// LogLevels returns a copy of the current log levels.
118func (c *Config) LogLevels() map[string]mlog.Level {
120 defer c.logMutex.Unlock()
121 return c.copyLogLevels()
124func (c *Config) withDynamicLock(fn func()) {
125 c.dynamicMutex.Lock()
126 defer c.dynamicMutex.Unlock()
128 if now.Sub(c.DynamicLastCheck) > time.Second {
129 c.DynamicLastCheck = now
130 if fi, err := os.Stat(ConfigDynamicPath); err != nil {
131 xlog.Errorx("stat domains config", err)
132 } else if !fi.ModTime().Equal(c.dynamicMtime) {
133 if errs := c.loadDynamic(); len(errs) > 0 {
134 xlog.Errorx("loading domains config", errs[0], mlog.Field("errors", errs))
136 xlog.Info("domains config reloaded")
137 c.dynamicMtime = fi.ModTime()
144// must be called with dynamic lock held.
145func (c *Config) loadDynamic() []error {
146 d, mtime, accDests, err := ParseDynamicConfig(context.Background(), ConfigDynamicPath, c.Static)
151 c.dynamicMtime = mtime
152 c.accountDestinations = accDests
153 c.allowACMEHosts(true)
157func (c *Config) Domains() (l []string) {
158 c.withDynamicLock(func() {
159 for name := range c.Dynamic.Domains {
163 sort.Slice(l, func(i, j int) bool {
169func (c *Config) Accounts() (l []string) {
170 c.withDynamicLock(func() {
171 for name := range c.Dynamic.Accounts {
178// DomainLocalparts returns a mapping of encoded localparts to account names for a
179// domain. An empty localpart is a catchall destination for a domain.
180func (c *Config) DomainLocalparts(d dns.Domain) map[string]string {
181 suffix := "@" + d.Name()
182 m := map[string]string{}
183 c.withDynamicLock(func() {
184 for addr, ad := range c.accountDestinations {
185 if strings.HasSuffix(addr, suffix) {
189 m[ad.Localpart.String()] = ad.Account
197func (c *Config) Domain(d dns.Domain) (dom config.Domain, ok bool) {
198 c.withDynamicLock(func() {
199 dom, ok = c.Dynamic.Domains[d.Name()]
204func (c *Config) Account(name string) (acc config.Account, ok bool) {
205 c.withDynamicLock(func() {
206 acc, ok = c.Dynamic.Accounts[name]
211func (c *Config) AccountDestination(addr string) (accDests AccountDestination, ok bool) {
212 c.withDynamicLock(func() {
213 accDests, ok = c.accountDestinations[addr]
218func (c *Config) WebServer() (r map[dns.Domain]dns.Domain, l []config.WebHandler) {
219 c.withDynamicLock(func() {
220 r = c.Dynamic.WebDNSDomainRedirects
221 l = c.Dynamic.WebHandlers
226func (c *Config) Routes(accountName string, domain dns.Domain) (accountRoutes, domainRoutes, globalRoutes []config.Route) {
227 c.withDynamicLock(func() {
228 acc := c.Dynamic.Accounts[accountName]
229 accountRoutes = acc.Routes
231 dom := c.Dynamic.Domains[domain.Name()]
232 domainRoutes = dom.Routes
234 globalRoutes = c.Dynamic.Routes
239func (c *Config) allowACMEHosts(checkACMEHosts bool) {
240 for _, l := range c.Static.Listeners {
241 if l.TLS == nil || l.TLS.ACME == "" {
245 m := c.Static.ACME[l.TLS.ACME].Manager
246 hostnames := map[dns.Domain]struct{}{}
248 hostnames[c.Static.HostnameDomain] = struct{}{}
249 if l.HostnameDomain.ASCII != "" {
250 hostnames[l.HostnameDomain] = struct{}{}
253 for _, dom := range c.Dynamic.Domains {
254 if dom.DMARC != nil && dom.DMARC.Domain != "" && dom.DMARC.DNSDomain != dom.Domain {
255 // Do not allow TLS certificates for domains for which we only accept DMARC reports
256 // as external party.
260 if l.AutoconfigHTTPS.Enabled && !l.AutoconfigHTTPS.NonTLS {
261 if d, err := dns.ParseDomain("autoconfig." + dom.Domain.ASCII); err != nil {
262 xlog.Errorx("parsing autoconfig domain", err, mlog.Field("domain", dom.Domain))
264 hostnames[d] = struct{}{}
268 if l.MTASTSHTTPS.Enabled && dom.MTASTS != nil && !l.MTASTSHTTPS.NonTLS {
269 d, err := dns.ParseDomain("mta-sts." + dom.Domain.ASCII)
271 xlog.Errorx("parsing mta-sts domain", err, mlog.Field("domain", dom.Domain))
273 hostnames[d] = struct{}{}
278 if l.WebserverHTTPS.Enabled {
279 for from := range c.Dynamic.WebDNSDomainRedirects {
280 hostnames[from] = struct{}{}
282 for _, wh := range c.Dynamic.WebHandlers {
283 hostnames[wh.DNSDomain] = struct{}{}
287 public := c.Static.Listeners["public"]
289 if len(public.NATIPs) > 0 {
295 m.SetAllowedHostnames(dns.StrictResolver{Pkg: "autotls"}, hostnames, ips, checkACMEHosts)
299// todo future: write config parsing & writing code that can read a config and remembers the exact tokens including newlines and comments, and can write back a modified file. the goal is to be able to write a config file automatically (after changing fields through the ui), but not loose comments and whitespace, to still get useful diffs for storing the config in a version control system.
301// must be called with lock held.
302func writeDynamic(ctx context.Context, log *mlog.Log, c config.Dynamic) error {
303 accDests, errs := prepareDynamicConfig(ctx, ConfigDynamicPath, Conf.Static, &c)
309 err := sconf.Write(&b, c)
313 f, err := os.OpenFile(ConfigDynamicPath, os.O_WRONLY, 0660)
320 log.Check(err, "closing file after error")
324 if _, err := f.Write(buf); err != nil {
325 return fmt.Errorf("write domains.conf: %v", err)
327 if err := f.Truncate(int64(len(buf))); err != nil {
328 return fmt.Errorf("truncate domains.conf after write: %v", err)
330 if err := f.Sync(); err != nil {
331 return fmt.Errorf("sync domains.conf after write: %v", err)
333 if err := moxio.SyncDir(filepath.Dir(ConfigDynamicPath)); err != nil {
334 return fmt.Errorf("sync dir of domains.conf after write: %v", err)
339 return fmt.Errorf("stat after writing domains.conf: %v", err)
342 if err := f.Close(); err != nil {
343 return fmt.Errorf("close written domains.conf: %v", err)
347 Conf.dynamicMtime = fi.ModTime()
348 Conf.DynamicLastCheck = time.Now()
350 Conf.accountDestinations = accDests
352 Conf.allowACMEHosts(true)
357// MustLoadConfig loads the config, quitting on errors.
358func MustLoadConfig(doLoadTLSKeyCerts, checkACMEHosts bool) {
359 errs := LoadConfig(context.Background(), doLoadTLSKeyCerts, checkACMEHosts)
361 xlog.Error("loading config file: multiple errors")
362 for _, err := range errs {
363 xlog.Errorx("config error", err)
365 xlog.Fatal("stopping after multiple config errors")
366 } else if len(errs) == 1 {
367 xlog.Fatalx("loading config file", errs[0])
371// LoadConfig attempts to parse and load a config, returning any errors
373func LoadConfig(ctx context.Context, doLoadTLSKeyCerts, checkACMEHosts bool) []error {
374 Shutdown, ShutdownCancel = context.WithCancel(context.Background())
375 Context, ContextCancel = context.WithCancel(context.Background())
377 c, errs := ParseConfig(ctx, ConfigStaticPath, false, doLoadTLSKeyCerts, checkACMEHosts)
382 mlog.SetConfig(c.Log)
387// SetConfig sets a new config. Not to be used during normal operation.
388func SetConfig(c *Config) {
389 // Cannot just assign *c to Conf, it would copy the mutex.
390 Conf = Config{c.Static, sync.Mutex{}, c.Log, sync.Mutex{}, c.Dynamic, c.dynamicMtime, c.DynamicLastCheck, c.accountDestinations}
392 // If we have non-standard CA roots, use them for all HTTPS requests.
393 if Conf.Static.TLS.CertPool != nil {
394 http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{
395 RootCAs: Conf.Static.TLS.CertPool,
399 moxvar.Pedantic = c.Static.Pedantic
402// ParseConfig parses the static config at path p. If checkOnly is true, no changes
403// are made, such as registering ACME identities. If doLoadTLSKeyCerts is true,
404// the TLS KeyCerts configuration is loaded and checked. This is used during the
405// quickstart in the case the user is going to provide their own certificates.
406// If checkACMEHosts is true, the hosts allowed for acme are compared with the
407// explicitly configured ips we are listening on.
408func ParseConfig(ctx context.Context, p string, checkOnly, doLoadTLSKeyCerts, checkACMEHosts bool) (c *Config, errs []error) {
410 Static: config.Static{
417 if os.IsNotExist(err) && os.Getenv("MOXCONF") == "" {
418 return nil, []error{fmt.Errorf("open config file: %v (hint: use mox -config ... or set MOXCONF=...)", err)}
420 return nil, []error{fmt.Errorf("open config file: %v", err)}
423 if err := sconf.Parse(f, &c.Static); err != nil {
424 return nil, []error{fmt.Errorf("parsing %s%v", p, err)}
427 if xerrs := PrepareStaticConfig(ctx, p, c, checkOnly, doLoadTLSKeyCerts); len(xerrs) > 0 {
431 pp := filepath.Join(filepath.Dir(p), "domains.conf")
432 c.Dynamic, c.dynamicMtime, c.accountDestinations, errs = ParseDynamicConfig(ctx, pp, c.Static)
435 c.allowACMEHosts(checkACMEHosts)
441// PrepareStaticConfig parses the static config file and prepares data structures
442// for starting mox. If checkOnly is set no substantial changes are made, like
443// creating an ACME registration.
444func PrepareStaticConfig(ctx context.Context, configFile string, conf *Config, checkOnly, doLoadTLSKeyCerts bool) (errs []error) {
445 addErrorf := func(format string, args ...any) {
446 errs = append(errs, fmt.Errorf(format, args...))
449 log := xlog.WithContext(ctx)
453 // check that mailbox is in unicode NFC normalized form.
454 checkMailboxNormf := func(mailbox string, format string, args ...any) {
455 s := norm.NFC.String(mailbox)
457 msg := fmt.Sprintf(format, args...)
458 addErrorf("%s: mailbox %q is not in NFC normalized form, should be %q", msg, mailbox, s)
462 // Post-process logging config.
463 if logLevel, ok := mlog.Levels[c.LogLevel]; ok {
464 conf.Log = map[string]mlog.Level{"": logLevel}
466 addErrorf("invalid log level %q", c.LogLevel)
468 for pkg, s := range c.PackageLogLevels {
469 if logLevel, ok := mlog.Levels[s]; ok {
470 conf.Log[pkg] = logLevel
472 addErrorf("invalid package log level %q", s)
479 u, err := user.Lookup(c.User)
481 uid, err := strconv.ParseUint(c.User, 10, 32)
483 addErrorf("parsing unknown user %s as uid: %v (hint: add user mox with \"useradd -d $PWD mox\" or specify a different username on the quickstart command-line)", c.User, err)
485 // We assume the same gid as uid.
490 if uid, err := strconv.ParseUint(u.Uid, 10, 32); err != nil {
491 addErrorf("parsing uid %s: %v", u.Uid, err)
495 if gid, err := strconv.ParseUint(u.Gid, 10, 32); err != nil {
496 addErrorf("parsing gid %s: %v", u.Gid, err)
502 hostname, err := dns.ParseDomain(c.Hostname)
504 addErrorf("parsing hostname: %s", err)
505 } else if hostname.Name() != c.Hostname {
506 addErrorf("hostname must be in IDNA form %q", hostname.Name())
508 c.HostnameDomain = hostname
510 if c.HostTLSRPT.Account != "" {
511 tlsrptLocalpart, err := smtp.ParseLocalpart(c.HostTLSRPT.Localpart)
513 addErrorf("invalid localpart %q for host tlsrpt: %v", c.HostTLSRPT.Localpart, err)
514 } else if tlsrptLocalpart.IsInternational() {
515 // Does not appear documented in
../rfc/8460, but similar to DMARC it makes sense
516 // to keep this ascii-only addresses.
517 addErrorf("host TLSRPT localpart %q is an internationalized address, only conventional ascii-only address allowed for interopability", tlsrptLocalpart)
519 c.HostTLSRPT.ParsedLocalpart = tlsrptLocalpart
522 // Return private key for host name for use with an ACME. Used to return the same
523 // private key as pre-generated for use with DANE, with its public key in DNS.
524 // We only use this key for Listener's that have this ACME configured, and for
525 // which the effective listener host name (either specific to the listener, or the
526 // global name) is requested. Other host names can get a fresh private key, they
527 // don't appear in DANE records.
529 // - run 0: only use listener with explicitly matching host name in listener
530 // (default quickstart config does not set it).
531 // - run 1: only look at public listener (and host matching mox host name)
532 // - run 2: all listeners (and host matching mox host name)
533 findACMEHostPrivateKey := func(acmeName, host string, keyType autocert.KeyType, run int) crypto.Signer {
534 for listenerName, l := range Conf.Static.Listeners {
535 if l.TLS == nil || l.TLS.ACME != acmeName {
538 if run == 0 && host != l.HostnameDomain.ASCII {
541 if run == 1 && listenerName != "public" || host != Conf.Static.HostnameDomain.ASCII {
545 case autocert.KeyRSA2048:
546 if len(l.TLS.HostPrivateRSA2048Keys) == 0 {
549 return l.TLS.HostPrivateRSA2048Keys[0]
550 case autocert.KeyECDSAP256:
551 if len(l.TLS.HostPrivateECDSAP256Keys) == 0 {
554 return l.TLS.HostPrivateECDSAP256Keys[0]
561 // Make a function for an autocert.Manager.GetPrivateKey, using findACMEHostPrivateKey.
562 makeGetPrivateKey := func(acmeName string) func(host string, keyType autocert.KeyType) (crypto.Signer, error) {
563 return func(host string, keyType autocert.KeyType) (crypto.Signer, error) {
564 key := findACMEHostPrivateKey(acmeName, host, keyType, 0)
566 key = findACMEHostPrivateKey(acmeName, host, keyType, 1)
569 key = findACMEHostPrivateKey(acmeName, host, keyType, 2)
572 log.Debug("found existing private key for certificate for host", mlog.Field("acmename", acmeName), mlog.Field("host", host), mlog.Field("keytype", keyType))
575 log.Debug("generating new private key for certificate for host", mlog.Field("acmename", acmeName), mlog.Field("host", host), mlog.Field("keytype", keyType))
577 case autocert.KeyRSA2048:
578 return rsa.GenerateKey(cryptorand.Reader, 2048)
579 case autocert.KeyECDSAP256:
580 return ecdsa.GenerateKey(elliptic.P256(), cryptorand.Reader)
582 return nil, fmt.Errorf("unrecognized requested key type %v", keyType)
586 for name, acme := range c.ACME {
590 acmeDir := dataDirPath(configFile, c.DataDir, "acme")
591 os.MkdirAll(acmeDir, 0770)
592 manager, err := autotls.Load(name, acmeDir, acme.ContactEmail, acme.DirectoryURL, makeGetPrivateKey(name), Shutdown.Done())
594 addErrorf("loading ACME identity for %q: %s", name, err)
596 acme.Manager = manager
600 var haveUnspecifiedSMTPListener bool
601 for name, l := range c.Listeners {
602 if l.Hostname != "" {
603 d, err := dns.ParseDomain(l.Hostname)
605 addErrorf("bad listener hostname %q: %s", l.Hostname, err)
610 if l.TLS.ACME != "" && len(l.TLS.KeyCerts) != 0 {
611 addErrorf("listener %q: cannot have ACME and static key/certificates", name)
612 } else if l.TLS.ACME != "" {
613 acme, ok := c.ACME[l.TLS.ACME]
615 addErrorf("listener %q: unknown ACME provider %q", name, l.TLS.ACME)
618 // If only checking or with missing ACME definition, we don't have an acme manager,
619 // so set an empty tls config to continue.
620 var tlsconfig *tls.Config
621 if checkOnly || acme.Manager == nil {
622 tlsconfig = &tls.Config{}
624 tlsconfig = acme.Manager.TLSConfig.Clone()
625 l.TLS.ACMEConfig = acme.Manager.ACMETLSConfig
627 // SMTP STARTTLS connections are commonly made without SNI, because certificates
628 // often aren't verified.
629 hostname := c.HostnameDomain
630 if l.Hostname != "" {
631 hostname = l.HostnameDomain
633 getCert := tlsconfig.GetCertificate
634 tlsconfig.GetCertificate = func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
635 if hello.ServerName == "" {
636 hello.ServerName = hostname.ASCII
638 return getCert(hello)
641 l.TLS.Config = tlsconfig
642 } else if len(l.TLS.KeyCerts) != 0 {
643 if doLoadTLSKeyCerts {
644 if err := loadTLSKeyCerts(configFile, "listener "+name, l.TLS); err != nil {
649 addErrorf("listener %q: cannot have TLS config without ACME and without static keys/certificates", name)
651 for _, privKeyFile := range l.TLS.HostPrivateKeyFiles {
652 keyPath := configDirPath(configFile, privKeyFile)
653 privKey, err := loadPrivateKeyFile(keyPath)
655 addErrorf("listener %q: parsing host private key for DANE and ACME certificates: %v", name, err)
658 switch k := privKey.(type) {
659 case *rsa.PrivateKey:
660 if k.N.BitLen() != 2048 {
661 log.Error("need rsa key with 2048 bits, for host private key for DANE/ACME certificates, ignoring", mlog.Field("listener", name), mlog.Field("file", keyPath), mlog.Field("bits", k.N.BitLen()))
664 l.TLS.HostPrivateRSA2048Keys = append(l.TLS.HostPrivateRSA2048Keys, k)
665 case *ecdsa.PrivateKey:
666 if k.Curve != elliptic.P256() {
667 log.Error("unrecognized ecdsa curve for host private key for DANE/ACME certificates, ignoring", mlog.Field("listener", name), mlog.Field("file", keyPath))
670 l.TLS.HostPrivateECDSAP256Keys = append(l.TLS.HostPrivateECDSAP256Keys, k)
672 log.Error("unrecognized key type for host private key for DANE/ACME certificates, ignoring", mlog.Field("listener", name), mlog.Field("file", keyPath), mlog.Field("keytype", fmt.Sprintf("%T", privKey)))
676 if l.TLS.ACME != "" && (len(l.TLS.HostPrivateRSA2048Keys) == 0) != (len(l.TLS.HostPrivateECDSAP256Keys) == 0) {
677 log.Error("warning: uncommon configuration with either only an RSA 2048 or ECDSA P256 host private key for DANE/ACME certificates; this ACME implementation can retrieve certificates for both type of keys, it is recommended to set either both or none; continuing")
681 var minVersion uint16 = tls.VersionTLS12
682 if l.TLS.MinVersion != "" {
683 versions := map[string]uint16{
684 "TLSv1.0": tls.VersionTLS10,
685 "TLSv1.1": tls.VersionTLS11,
686 "TLSv1.2": tls.VersionTLS12,
687 "TLSv1.3": tls.VersionTLS13,
689 v, ok := versions[l.TLS.MinVersion]
691 addErrorf("listener %q: unknown TLS mininum version %q", name, l.TLS.MinVersion)
695 if l.TLS.Config != nil {
696 l.TLS.Config.MinVersion = minVersion
698 if l.TLS.ACMEConfig != nil {
699 l.TLS.ACMEConfig.MinVersion = minVersion
702 var needsTLS []string
703 needtls := func(s string, v bool) {
705 needsTLS = append(needsTLS, s)
708 needtls("IMAPS", l.IMAPS.Enabled)
709 needtls("SMTP", l.SMTP.Enabled && !l.SMTP.NoSTARTTLS)
710 needtls("Submissions", l.Submissions.Enabled)
711 needtls("Submission", l.Submission.Enabled && !l.Submission.NoRequireSTARTTLS)
712 needtls("AccountHTTPS", l.AccountHTTPS.Enabled)
713 needtls("AdminHTTPS", l.AdminHTTPS.Enabled)
714 needtls("AutoconfigHTTPS", l.AutoconfigHTTPS.Enabled && !l.AutoconfigHTTPS.NonTLS)
715 needtls("MTASTSHTTPS", l.MTASTSHTTPS.Enabled && !l.MTASTSHTTPS.NonTLS)
716 needtls("WebserverHTTPS", l.WebserverHTTPS.Enabled)
717 if len(needsTLS) > 0 {
718 addErrorf("listener %q does not specify tls config, but requires tls for %s", name, strings.Join(needsTLS, ", "))
721 if l.AutoconfigHTTPS.Enabled && l.MTASTSHTTPS.Enabled && l.AutoconfigHTTPS.Port == l.MTASTSHTTPS.Port && l.AutoconfigHTTPS.NonTLS != l.MTASTSHTTPS.NonTLS {
722 addErrorf("listener %q tries to enable autoconfig and mta-sts enabled on same port but with both http and https", name)
726 haveUnspecifiedSMTPListener = true
728 for _, ipstr := range l.IPs {
729 ip := net.ParseIP(ipstr)
731 addErrorf("listener %q has invalid IP %q", name, ipstr)
734 if ip.IsUnspecified() {
735 haveUnspecifiedSMTPListener = true
738 if len(c.SpecifiedSMTPListenIPs) >= 2 {
739 haveUnspecifiedSMTPListener = true
740 } else if len(c.SpecifiedSMTPListenIPs) > 0 && (c.SpecifiedSMTPListenIPs[0].To4() == nil) == (ip.To4() == nil) {
741 haveUnspecifiedSMTPListener = true
743 c.SpecifiedSMTPListenIPs = append(c.SpecifiedSMTPListenIPs, ip)
747 for _, s := range l.SMTP.DNSBLs {
748 d, err := dns.ParseDomain(s)
750 addErrorf("listener %q has invalid DNSBL zone %q", name, s)
753 l.SMTP.DNSBLZones = append(l.SMTP.DNSBLZones, d)
755 if l.IPsNATed && len(l.NATIPs) > 0 {
756 addErrorf("listener %q has both IPsNATed and NATIPs (remove deprecated IPsNATed)", name)
758 for _, ipstr := range l.NATIPs {
759 ip := net.ParseIP(ipstr)
761 addErrorf("listener %q has invalid ip %q", name, ipstr)
762 } else if ip.IsUnspecified() || ip.IsLoopback() {
763 addErrorf("listener %q has NAT ip that is the unspecified or loopback address %s", name, ipstr)
766 checkPath := func(kind string, enabled bool, path string) {
767 if enabled && path != "" && !strings.HasPrefix(path, "/") {
768 addErrorf("listener %q has %s with path %q that must start with a slash", name, kind, path)
771 checkPath("AccountHTTP", l.AccountHTTP.Enabled, l.AccountHTTP.Path)
772 checkPath("AccountHTTPS", l.AccountHTTPS.Enabled, l.AccountHTTPS.Path)
773 checkPath("AdminHTTP", l.AdminHTTP.Enabled, l.AdminHTTP.Path)
774 checkPath("AdminHTTPS", l.AdminHTTPS.Enabled, l.AdminHTTPS.Path)
775 c.Listeners[name] = l
777 if haveUnspecifiedSMTPListener {
778 c.SpecifiedSMTPListenIPs = nil
781 var zerouse config.SpecialUseMailboxes
782 if len(c.DefaultMailboxes) > 0 && (c.InitialMailboxes.SpecialUse != zerouse || len(c.InitialMailboxes.Regular) > 0) {
783 addErrorf("cannot have both DefaultMailboxes and InitialMailboxes")
785 // DefaultMailboxes is deprecated.
786 for _, mb := range c.DefaultMailboxes {
787 checkMailboxNormf(mb, "default mailbox")
789 checkSpecialUseMailbox := func(nameOpt string) {
791 checkMailboxNormf(nameOpt, "special-use initial mailbox")
792 if strings.EqualFold(nameOpt, "inbox") {
793 addErrorf("initial mailbox cannot be set to Inbox (Inbox is always created)")
797 checkSpecialUseMailbox(c.InitialMailboxes.SpecialUse.Archive)
798 checkSpecialUseMailbox(c.InitialMailboxes.SpecialUse.Draft)
799 checkSpecialUseMailbox(c.InitialMailboxes.SpecialUse.Junk)
800 checkSpecialUseMailbox(c.InitialMailboxes.SpecialUse.Sent)
801 checkSpecialUseMailbox(c.InitialMailboxes.SpecialUse.Trash)
802 for _, name := range c.InitialMailboxes.Regular {
803 checkMailboxNormf(name, "regular initial mailbox")
804 if strings.EqualFold(name, "inbox") {
805 addErrorf("initial regular mailbox cannot be set to Inbox (Inbox is always created)")
809 checkTransportSMTP := func(name string, isTLS bool, t *config.TransportSMTP) {
811 t.DNSHost, err = dns.ParseDomain(t.Host)
813 addErrorf("transport %s: bad host %s: %v", name, t.Host, err)
816 if isTLS && t.STARTTLSInsecureSkipVerify {
817 addErrorf("transport %s: cannot have STARTTLSInsecureSkipVerify with immediate TLS")
819 if isTLS && t.NoSTARTTLS {
820 addErrorf("transport %s: cannot have NoSTARTTLS with immediate TLS")
826 seen := map[string]bool{}
827 for _, m := range t.Auth.Mechanisms {
829 addErrorf("transport %s: duplicate authentication mechanism %s", name, m)
833 case "SCRAM-SHA-256":
838 addErrorf("transport %s: unknown authentication mechanism %s", name, m)
842 t.Auth.EffectiveMechanisms = t.Auth.Mechanisms
843 if len(t.Auth.EffectiveMechanisms) == 0 {
844 t.Auth.EffectiveMechanisms = []string{"SCRAM-SHA-256", "SCRAM-SHA-1", "CRAM-MD5"}
848 checkTransportSocks := func(name string, t *config.TransportSocks) {
849 _, _, err := net.SplitHostPort(t.Address)
851 addErrorf("transport %s: bad address %s: %v", name, t.Address, err)
853 for _, ipstr := range t.RemoteIPs {
854 ip := net.ParseIP(ipstr)
856 addErrorf("transport %s: bad ip %s", name, ipstr)
858 t.IPs = append(t.IPs, ip)
861 t.Hostname, err = dns.ParseDomain(t.RemoteHostname)
863 addErrorf("transport %s: bad hostname %s: %v", name, t.RemoteHostname, err)
867 for name, t := range c.Transports {
869 if t.Submissions != nil {
871 checkTransportSMTP(name, true, t.Submissions)
873 if t.Submission != nil {
875 checkTransportSMTP(name, false, t.Submission)
879 checkTransportSMTP(name, false, t.SMTP)
883 checkTransportSocks(name, t.Socks)
886 addErrorf("transport %s: cannot have multiple methods in a transport", name)
890 // Load CA certificate pool.
892 if c.TLS.CA.AdditionalToSystem {
894 c.TLS.CertPool, err = x509.SystemCertPool()
896 addErrorf("fetching system CA cert pool: %v", err)
899 c.TLS.CertPool = x509.NewCertPool()
901 for _, certfile := range c.TLS.CA.CertFiles {
902 p := configDirPath(configFile, certfile)
903 pemBuf, err := os.ReadFile(p)
905 addErrorf("reading TLS CA cert file: %v", err)
907 } else if !c.TLS.CertPool.AppendCertsFromPEM(pemBuf) {
908 // todo: can we check more fully if we're getting some useful data back?
909 addErrorf("no CA certs added from %q", p)
916// PrepareDynamicConfig parses the dynamic config file given a static file.
917func ParseDynamicConfig(ctx context.Context, dynamicPath string, static config.Static) (c config.Dynamic, mtime time.Time, accDests map[string]AccountDestination, errs []error) {
918 addErrorf := func(format string, args ...any) {
919 errs = append(errs, fmt.Errorf(format, args...))
922 f, err := os.Open(dynamicPath)
924 addErrorf("parsing domains config: %v", err)
930 addErrorf("stat domains config: %v", err)
932 if err := sconf.Parse(f, &c); err != nil {
933 addErrorf("parsing dynamic config file: %v", err)
937 accDests, errs = prepareDynamicConfig(ctx, dynamicPath, static, &c)
938 return c, fi.ModTime(), accDests, errs
941func prepareDynamicConfig(ctx context.Context, dynamicPath string, static config.Static, c *config.Dynamic) (accDests map[string]AccountDestination, errs []error) {
942 log := xlog.WithContext(ctx)
944 addErrorf := func(format string, args ...any) {
945 errs = append(errs, fmt.Errorf(format, args...))
948 // Check that mailbox is in unicode NFC normalized form.
949 checkMailboxNormf := func(mailbox string, format string, args ...any) {
950 s := norm.NFC.String(mailbox)
952 msg := fmt.Sprintf(format, args...)
953 addErrorf("%s: mailbox %q is not in NFC normalized form, should be %q", msg, mailbox, s)
957 // Validate postmaster account exists.
958 if _, ok := c.Accounts[static.Postmaster.Account]; !ok {
959 addErrorf("postmaster account %q does not exist", static.Postmaster.Account)
961 checkMailboxNormf(static.Postmaster.Mailbox, "postmaster mailbox")
963 accDests = map[string]AccountDestination{}
965 // Validate host TLSRPT account/address.
966 if static.HostTLSRPT.Account != "" {
967 if _, ok := c.Accounts[static.HostTLSRPT.Account]; !ok {
968 addErrorf("host tlsrpt account %q does not exist", static.HostTLSRPT.Account)
970 checkMailboxNormf(static.HostTLSRPT.Mailbox, "host tlsrpt mailbox")
972 // Localpart has been parsed already.
974 addrFull := smtp.NewAddress(static.HostTLSRPT.ParsedLocalpart, static.HostnameDomain).String()
975 dest := config.Destination{
976 Mailbox: static.HostTLSRPT.Mailbox,
977 HostTLSReports: true,
979 accDests[addrFull] = AccountDestination{false, static.HostTLSRPT.ParsedLocalpart, static.HostTLSRPT.Account, dest}
982 var haveSTSListener, haveWebserverListener bool
983 for _, l := range static.Listeners {
984 if l.MTASTSHTTPS.Enabled {
985 haveSTSListener = true
987 if l.WebserverHTTP.Enabled || l.WebserverHTTPS.Enabled {
988 haveWebserverListener = true
992 checkRoutes := func(descr string, routes []config.Route) {
993 parseRouteDomains := func(l []string) []string {
995 for _, e := range l {
1001 if strings.HasPrefix(e, ".") {
1005 d, err := dns.ParseDomain(e)
1007 addErrorf("%s: invalid domain %s: %v", descr, e, err)
1009 r = append(r, prefix+d.ASCII)
1014 for i := range routes {
1015 routes[i].FromDomainASCII = parseRouteDomains(routes[i].FromDomain)
1016 routes[i].ToDomainASCII = parseRouteDomains(routes[i].ToDomain)
1018 routes[i].ResolvedTransport, ok = static.Transports[routes[i].Transport]
1020 addErrorf("%s: route references undefined transport %s", descr, routes[i].Transport)
1025 checkRoutes("global routes", c.Routes)
1027 // Validate domains.
1028 for d, domain := range c.Domains {
1029 dnsdomain, err := dns.ParseDomain(d)
1031 addErrorf("bad domain %q: %s", d, err)
1032 } else if dnsdomain.Name() != d {
1033 addErrorf("domain %s must be specified in IDNA form, %s", d, dnsdomain.Name())
1036 domain.Domain = dnsdomain
1038 for _, sign := range domain.DKIM.Sign {
1039 if _, ok := domain.DKIM.Selectors[sign]; !ok {
1040 addErrorf("selector %s for signing is missing in domain %s", sign, d)
1043 for name, sel := range domain.DKIM.Selectors {
1044 seld, err := dns.ParseDomain(name)
1046 addErrorf("bad selector %q: %s", name, err)
1047 } else if seld.Name() != name {
1048 addErrorf("selector %q must be specified in IDNA form, %q", name, seld.Name())
1052 if sel.Expiration != "" {
1053 exp, err := time.ParseDuration(sel.Expiration)
1055 addErrorf("selector %q has invalid expiration %q: %v", name, sel.Expiration, err)
1057 sel.ExpirationSeconds = int(exp / time.Second)
1061 sel.HashEffective = sel.Hash
1062 switch sel.HashEffective {
1064 sel.HashEffective = "sha256"
1066 log.Error("using sha1 with DKIM is deprecated as not secure enough, switch to sha256")
1069 addErrorf("unsupported hash %q for selector %q in domain %s", sel.HashEffective, name, d)
1072 pemBuf, err := os.ReadFile(configDirPath(dynamicPath, sel.PrivateKeyFile))
1074 addErrorf("reading private key for selector %s in domain %s: %s", name, d, err)
1077 p, _ := pem.Decode(pemBuf)
1079 addErrorf("private key for selector %s in domain %s has no PEM block", name, d)
1082 key, err := x509.ParsePKCS8PrivateKey(p.Bytes)
1084 addErrorf("parsing private key for selector %s in domain %s: %s", name, d, err)
1087 switch k := key.(type) {
1088 case *rsa.PrivateKey:
1089 if k.N.BitLen() < 1024 {
1091 // Let's help user do the right thing.
1092 addErrorf("rsa keys should be >= 1024 bits")
1095 case ed25519.PrivateKey:
1096 if sel.HashEffective != "sha256" {
1097 addErrorf("hash algorithm %q is not supported with ed25519, only sha256 is", sel.HashEffective)
1101 addErrorf("private key type %T not yet supported, at selector %s in domain %s", key, name, d)
1104 if len(sel.Headers) == 0 {
1108 // By default we seal signed headers, and we sign user-visible headers to
1109 // prevent/limit reuse of previously signed messages: All addressing fields, date
1110 // and subject, message-referencing fields, parsing instructions (content-type).
1111 sel.HeadersEffective = strings.Split("From,To,Cc,Bcc,Reply-To,References,In-Reply-To,Subject,Date,Message-Id,Content-Type", ",")
1114 for _, h := range sel.Headers {
1115 from = from || strings.EqualFold(h, "From")
1117 if strings.EqualFold(h, "DKIM-Signature") || strings.EqualFold(h, "Received") || strings.EqualFold(h, "Return-Path") {
1118 log.Error("DKIM-signing header %q is recommended against as it may be modified in transit")
1122 addErrorf("From-field must always be DKIM-signed")
1124 sel.HeadersEffective = sel.Headers
1127 domain.DKIM.Selectors[name] = sel
1130 if domain.MTASTS != nil {
1131 if !haveSTSListener {
1132 addErrorf("MTA-STS enabled for domain %q, but there is no listener for MTASTS", d)
1134 sts := domain.MTASTS
1135 if sts.PolicyID == "" {
1136 addErrorf("invalid empty MTA-STS PolicyID")
1139 case mtasts.ModeNone, mtasts.ModeTesting, mtasts.ModeEnforce:
1141 addErrorf("invalid mtasts mode %q", sts.Mode)
1145 checkRoutes("routes for domain", domain.Routes)
1147 c.Domains[d] = domain
1150 // Validate email addresses.
1151 for accName, acc := range c.Accounts {
1153 acc.DNSDomain, err = dns.ParseDomain(acc.Domain)
1155 addErrorf("parsing domain %s for account %q: %s", acc.Domain, accName, err)
1158 if strings.EqualFold(acc.RejectsMailbox, "Inbox") {
1159 addErrorf("account %q: cannot set RejectsMailbox to inbox, messages will be removed automatically from the rejects mailbox", accName)
1161 checkMailboxNormf(acc.RejectsMailbox, "account %q", accName)
1163 if acc.AutomaticJunkFlags.JunkMailboxRegexp != "" {
1164 r, err := regexp.Compile(acc.AutomaticJunkFlags.JunkMailboxRegexp)
1166 addErrorf("invalid JunkMailboxRegexp regular expression: %v", err)
1170 if acc.AutomaticJunkFlags.NeutralMailboxRegexp != "" {
1171 r, err := regexp.Compile(acc.AutomaticJunkFlags.NeutralMailboxRegexp)
1173 addErrorf("invalid NeutralMailboxRegexp regular expression: %v", err)
1175 acc.NeutralMailbox = r
1177 if acc.AutomaticJunkFlags.NotJunkMailboxRegexp != "" {
1178 r, err := regexp.Compile(acc.AutomaticJunkFlags.NotJunkMailboxRegexp)
1180 addErrorf("invalid NotJunkMailboxRegexp regular expression: %v", err)
1182 acc.NotJunkMailbox = r
1184 c.Accounts[accName] = acc
1186 // todo deprecated: only localpart as keys for Destinations, we are replacing them with full addresses. if domains.conf is written, we won't have to do this again.
1187 replaceLocalparts := map[string]string{}
1189 for addrName, dest := range acc.Destinations {
1190 checkMailboxNormf(dest.Mailbox, "account %q, destination %q", accName, addrName)
1192 for i, rs := range dest.Rulesets {
1193 checkMailboxNormf(rs.Mailbox, "account %q, destination %q, ruleset %d", accName, addrName, i+1)
1197 if rs.SMTPMailFromRegexp != "" {
1199 r, err := regexp.Compile(rs.SMTPMailFromRegexp)
1201 addErrorf("invalid SMTPMailFrom regular expression: %v", err)
1203 c.Accounts[accName].Destinations[addrName].Rulesets[i].SMTPMailFromRegexpCompiled = r
1205 if rs.VerifiedDomain != "" {
1207 d, err := dns.ParseDomain(rs.VerifiedDomain)
1209 addErrorf("invalid VerifiedDomain: %v", err)
1211 c.Accounts[accName].Destinations[addrName].Rulesets[i].VerifiedDNSDomain = d
1214 var hdr [][2]*regexp.Regexp
1215 for k, v := range rs.HeadersRegexp {
1217 if strings.ToLower(k) != k {
1218 addErrorf("header field %q must only have lower case characters", k)
1220 if strings.ToLower(v) != v {
1221 addErrorf("header value %q must only have lower case characters", v)
1223 rk, err := regexp.Compile(k)
1225 addErrorf("invalid rule header regexp %q: %v", k, err)
1227 rv, err := regexp.Compile(v)
1229 addErrorf("invalid rule header regexp %q: %v", v, err)
1231 hdr = append(hdr, [...]*regexp.Regexp{rk, rv})
1233 c.Accounts[accName].Destinations[addrName].Rulesets[i].HeadersRegexpCompiled = hdr
1236 addErrorf("ruleset must have at least one rule")
1239 if rs.IsForward && rs.ListAllowDomain != "" {
1240 addErrorf("ruleset cannot have both IsForward and ListAllowDomain")
1243 if rs.SMTPMailFromRegexp == "" || rs.VerifiedDomain == "" {
1244 addErrorf("ruleset with IsForward must have both SMTPMailFromRegexp and VerifiedDomain too")
1247 if rs.ListAllowDomain != "" {
1248 d, err := dns.ParseDomain(rs.ListAllowDomain)
1250 addErrorf("invalid ListAllowDomain %q: %v", rs.ListAllowDomain, err)
1252 c.Accounts[accName].Destinations[addrName].Rulesets[i].ListAllowDNSDomain = d
1255 checkMailboxNormf(rs.AcceptRejectsToMailbox, "account %q, destination %q, ruleset %d, rejects mailbox", accName, addrName, i+1)
1256 if strings.EqualFold(rs.AcceptRejectsToMailbox, "inbox") {
1257 addErrorf("account %q, destination %q, ruleset %d: AcceptRejectsToMailbox cannot be set to Inbox", accName, addrName, i+1)
1261 // Catchall destination for domain.
1262 if strings.HasPrefix(addrName, "@") {
1263 d, err := dns.ParseDomain(addrName[1:])
1265 addErrorf("parsing domain %q in account %q", addrName[1:], accName)
1267 } else if _, ok := c.Domains[d.Name()]; !ok {
1268 addErrorf("unknown domain for address %q in account %q", addrName, accName)
1271 addrFull := "@" + d.Name()
1272 if _, ok := accDests[addrFull]; ok {
1273 addErrorf("duplicate canonicalized catchall destination address %s", addrFull)
1275 accDests[addrFull] = AccountDestination{true, "", accName, dest}
1279 // todo deprecated: remove support for parsing destination as just a localpart instead full address.
1280 var address smtp.Address
1281 if localpart, err := smtp.ParseLocalpart(addrName); err != nil && errors.Is(err, smtp.ErrBadLocalpart) {
1282 address, err = smtp.ParseAddress(addrName)
1284 addErrorf("invalid email address %q in account %q", addrName, accName)
1286 } else if _, ok := c.Domains[address.Domain.Name()]; !ok {
1287 addErrorf("unknown domain for address %q in account %q", addrName, accName)
1292 addErrorf("invalid localpart %q in account %q", addrName, accName)
1295 address = smtp.NewAddress(localpart, acc.DNSDomain)
1296 if _, ok := c.Domains[acc.DNSDomain.Name()]; !ok {
1297 addErrorf("unknown domain %s for account %q", acc.DNSDomain.Name(), accName)
1300 replaceLocalparts[addrName] = address.Pack(true)
1303 origLP := address.Localpart
1304 dc := c.Domains[address.Domain.Name()]
1305 if lp, err := CanonicalLocalpart(address.Localpart, dc); err != nil {
1306 addErrorf("canonicalizing localpart %s: %v", address.Localpart, err)
1307 } else if dc.LocalpartCatchallSeparator != "" && strings.Contains(string(address.Localpart), dc.LocalpartCatchallSeparator) {
1308 addErrorf("localpart of address %s includes domain catchall separator %s", address, dc.LocalpartCatchallSeparator)
1310 address.Localpart = lp
1312 addrFull := address.Pack(true)
1313 if _, ok := accDests[addrFull]; ok {
1314 addErrorf("duplicate canonicalized destination address %s", addrFull)
1316 accDests[addrFull] = AccountDestination{false, origLP, accName, dest}
1319 for lp, addr := range replaceLocalparts {
1320 dest, ok := acc.Destinations[lp]
1322 addErrorf("could not find localpart %q to replace with address in destinations", lp)
1324 log.Error(`deprecation warning: support for account destination addresses specified as just localpart ("username") instead of full email address will be removed in the future; update domains.conf, for each Account, for each Destination, ensure each key is an email address by appending "@" and the default domain for the account`, mlog.Field("localpart", lp), mlog.Field("address", addr), mlog.Field("account", accName))
1325 acc.Destinations[addr] = dest
1326 delete(acc.Destinations, lp)
1330 checkRoutes("routes for account", acc.Routes)
1333 // Set DMARC destinations.
1334 for d, domain := range c.Domains {
1335 dmarc := domain.DMARC
1339 if _, ok := c.Accounts[dmarc.Account]; !ok {
1340 addErrorf("DMARC account %q does not exist", dmarc.Account)
1342 lp, err := smtp.ParseLocalpart(dmarc.Localpart)
1344 addErrorf("invalid DMARC localpart %q: %s", dmarc.Localpart, err)
1346 if lp.IsInternational() {
1348 addErrorf("DMARC localpart %q is an internationalized address, only conventional ascii-only address possible for interopability", lp)
1350 addrdom := domain.Domain
1351 if dmarc.Domain != "" {
1352 addrdom, err = dns.ParseDomain(dmarc.Domain)
1354 addErrorf("DMARC domain %q: %s", dmarc.Domain, err)
1355 } else if _, ok := c.Domains[addrdom.Name()]; !ok {
1356 addErrorf("unknown domain %q for DMARC address in domain %q", dmarc.Domain, d)
1360 domain.DMARC.ParsedLocalpart = lp
1361 domain.DMARC.DNSDomain = addrdom
1362 c.Domains[d] = domain
1363 addrFull := smtp.NewAddress(lp, addrdom).String()
1364 dest := config.Destination{
1365 Mailbox: dmarc.Mailbox,
1368 checkMailboxNormf(dmarc.Mailbox, "DMARC mailbox for account %q", dmarc.Account)
1369 accDests[addrFull] = AccountDestination{false, lp, dmarc.Account, dest}
1372 // Set TLSRPT destinations.
1373 for d, domain := range c.Domains {
1374 tlsrpt := domain.TLSRPT
1378 if _, ok := c.Accounts[tlsrpt.Account]; !ok {
1379 addErrorf("TLSRPT account %q does not exist", tlsrpt.Account)
1381 lp, err := smtp.ParseLocalpart(tlsrpt.Localpart)
1383 addErrorf("invalid TLSRPT localpart %q: %s", tlsrpt.Localpart, err)
1385 if lp.IsInternational() {
1386 // Does not appear documented in
../rfc/8460, but similar to DMARC it makes sense
1387 // to keep this ascii-only addresses.
1388 addErrorf("TLSRPT localpart %q is an internationalized address, only conventional ascii-only address allowed for interopability", lp)
1390 addrdom := domain.Domain
1391 if tlsrpt.Domain != "" {
1392 addrdom, err = dns.ParseDomain(tlsrpt.Domain)
1394 addErrorf("TLSRPT domain %q: %s", tlsrpt.Domain, err)
1395 } else if _, ok := c.Domains[addrdom.Name()]; !ok {
1396 addErrorf("unknown domain %q for TLSRPT address in domain %q", tlsrpt.Domain, d)
1400 domain.TLSRPT.ParsedLocalpart = lp
1401 domain.TLSRPT.DNSDomain = addrdom
1402 c.Domains[d] = domain
1403 addrFull := smtp.NewAddress(lp, addrdom).String()
1404 dest := config.Destination{
1405 Mailbox: tlsrpt.Mailbox,
1406 DomainTLSReports: true,
1408 checkMailboxNormf(tlsrpt.Mailbox, "TLSRPT mailbox for account %q", tlsrpt.Account)
1409 accDests[addrFull] = AccountDestination{false, lp, tlsrpt.Account, dest}
1412 // Check webserver configs.
1413 if (len(c.WebDomainRedirects) > 0 || len(c.WebHandlers) > 0) && !haveWebserverListener {
1414 addErrorf("WebDomainRedirects or WebHandlers configured but no listener with WebserverHTTP or WebserverHTTPS enabled")
1417 c.WebDNSDomainRedirects = map[dns.Domain]dns.Domain{}
1418 for from, to := range c.WebDomainRedirects {
1419 fromdom, err := dns.ParseDomain(from)
1421 addErrorf("parsing domain for redirect %s: %v", from, err)
1423 todom, err := dns.ParseDomain(to)
1425 addErrorf("parsing domain for redirect %s: %v", to, err)
1426 } else if fromdom == todom {
1427 addErrorf("will not redirect domain %s to itself", todom)
1429 var zerodom dns.Domain
1430 if _, ok := c.WebDNSDomainRedirects[fromdom]; ok && fromdom != zerodom {
1431 addErrorf("duplicate redirect domain %s", from)
1433 c.WebDNSDomainRedirects[fromdom] = todom
1436 for i := range c.WebHandlers {
1437 wh := &c.WebHandlers[i]
1439 if wh.LogName == "" {
1440 wh.Name = fmt.Sprintf("%d", i)
1442 wh.Name = wh.LogName
1445 dom, err := dns.ParseDomain(wh.Domain)
1447 addErrorf("webhandler %s %s: parsing domain: %v", wh.Domain, wh.PathRegexp, err)
1451 if !strings.HasPrefix(wh.PathRegexp, "^") {
1452 addErrorf("webhandler %s %s: path regexp must start with a ^", wh.Domain, wh.PathRegexp)
1454 re, err := regexp.Compile(wh.PathRegexp)
1456 addErrorf("webhandler %s %s: compiling regexp: %v", wh.Domain, wh.PathRegexp, err)
1461 if wh.WebStatic != nil {
1464 if ws.StripPrefix != "" && !strings.HasPrefix(ws.StripPrefix, "/") {
1465 addErrorf("webstatic %s %s: prefix to strip %s must start with a slash", wh.Domain, wh.PathRegexp, ws.StripPrefix)
1467 for k := range ws.ResponseHeaders {
1469 k := strings.TrimSpace(xk)
1470 if k != xk || k == "" {
1471 addErrorf("webstatic %s %s: bad header %q", wh.Domain, wh.PathRegexp, xk)
1475 if wh.WebRedirect != nil {
1477 wr := wh.WebRedirect
1478 if wr.BaseURL != "" {
1479 u, err := url.Parse(wr.BaseURL)
1481 addErrorf("webredirect %s %s: parsing redirect url %s: %v", wh.Domain, wh.PathRegexp, wr.BaseURL, err)
1487 addErrorf("webredirect %s %s: BaseURL must have empty path", wh.Domain, wh.PathRegexp, wr.BaseURL)
1491 if wr.OrigPathRegexp != "" && wr.ReplacePath != "" {
1492 re, err := regexp.Compile(wr.OrigPathRegexp)
1494 addErrorf("webredirect %s %s: compiling regexp %s: %v", wh.Domain, wh.PathRegexp, wr.OrigPathRegexp, err)
1497 } else if wr.OrigPathRegexp != "" || wr.ReplacePath != "" {
1498 addErrorf("webredirect %s %s: must have either both OrigPathRegexp and ReplacePath, or neither", wh.Domain, wh.PathRegexp)
1499 } else if wr.BaseURL == "" {
1500 addErrorf("webredirect %s %s: must at least one of BaseURL and OrigPathRegexp+ReplacePath", wh.Domain, wh.PathRegexp)
1502 if wr.StatusCode != 0 && (wr.StatusCode < 300 || wr.StatusCode >= 400) {
1503 addErrorf("webredirect %s %s: invalid redirect status code %d", wh.Domain, wh.PathRegexp, wr.StatusCode)
1506 if wh.WebForward != nil {
1509 u, err := url.Parse(wf.URL)
1511 addErrorf("webforward %s %s: parsing url %s: %v", wh.Domain, wh.PathRegexp, wf.URL, err)
1515 for k := range wf.ResponseHeaders {
1517 k := strings.TrimSpace(xk)
1518 if k != xk || k == "" {
1519 addErrorf("webforward %s %s: bad header %q", wh.Domain, wh.PathRegexp, xk)
1524 addErrorf("webhandler %s %s: must have exactly one handler, not %d", wh.Domain, wh.PathRegexp, n)
1531func loadPrivateKeyFile(keyPath string) (crypto.Signer, error) {
1532 keyBuf, err := os.ReadFile(keyPath)
1534 return nil, fmt.Errorf("reading host private key: %v", err)
1536 b, _ := pem.Decode(keyBuf)
1538 return nil, fmt.Errorf("parsing pem block for private key: %v", err)
1543 privKey, err = x509.ParsePKCS8PrivateKey(b.Bytes)
1544 case "RSA PRIVATE KEY":
1545 privKey, err = x509.ParsePKCS1PrivateKey(b.Bytes)
1546 case "EC PRIVATE KEY":
1547 privKey, err = x509.ParseECPrivateKey(b.Bytes)
1549 err = fmt.Errorf("unknown pem type %q", b.Type)
1552 return nil, fmt.Errorf("parsing private key: %v", err)
1554 if k, ok := privKey.(crypto.Signer); ok {
1557 return nil, fmt.Errorf("parsed private key not a crypto.Signer, but %T", privKey)
1560func loadTLSKeyCerts(configFile, kind string, ctls *config.TLS) error {
1561 certs := []tls.Certificate{}
1562 for _, kp := range ctls.KeyCerts {
1563 certPath := configDirPath(configFile, kp.CertFile)
1564 keyPath := configDirPath(configFile, kp.KeyFile)
1565 cert, err := loadX509KeyPairPrivileged(certPath, keyPath)
1567 return fmt.Errorf("tls config for %q: parsing x509 key pair: %v", kind, err)
1569 certs = append(certs, cert)
1571 ctls.Config = &tls.Config{
1572 Certificates: certs,
1577// load x509 key/cert files from file descriptor possibly passed in by privileged
1579func loadX509KeyPairPrivileged(certPath, keyPath string) (tls.Certificate, error) {
1580 certBuf, err := readFilePrivileged(certPath)
1582 return tls.Certificate{}, fmt.Errorf("reading tls certificate: %v", err)
1584 keyBuf, err := readFilePrivileged(keyPath)
1586 return tls.Certificate{}, fmt.Errorf("reading tls key: %v", err)
1588 return tls.X509KeyPair(certBuf, keyBuf)
1591// like os.ReadFile, but open privileged file possibly passed in by root process.
1592func readFilePrivileged(path string) ([]byte, error) {
1593 f, err := OpenPrivileged(path)
1598 return io.ReadAll(f)