8 cryptorand "crypto/rand"
25 "golang.org/x/exp/maps"
27 "github.com/mjl-/adns"
29 "github.com/mjl-/mox/config"
30 "github.com/mjl-/mox/dkim"
31 "github.com/mjl-/mox/dmarc"
32 "github.com/mjl-/mox/dns"
33 "github.com/mjl-/mox/junk"
34 "github.com/mjl-/mox/mlog"
35 "github.com/mjl-/mox/mtasts"
36 "github.com/mjl-/mox/smtp"
37 "github.com/mjl-/mox/tlsrpt"
40var ErrRequest = errors.New("bad request")
42// TXTStrings returns a TXT record value as one or more quoted strings, each max
43// 100 characters. In case of multiple strings, a multi-line record is returned.
44func TXTStrings(s string) string {
58 r += "\t\t\"" + s[:n] + "\"\n"
65// MakeDKIMEd25519Key returns a PEM buffer containing an ed25519 key for use
67// selector and domain can be empty. If not, they are used in the note.
68func MakeDKIMEd25519Key(selector, domain dns.Domain) ([]byte, error) {
69 _, privKey, err := ed25519.GenerateKey(cryptorand.Reader)
71 return nil, fmt.Errorf("generating key: %w", err)
74 pkcs8, err := x509.MarshalPKCS8PrivateKey(privKey)
76 return nil, fmt.Errorf("marshal key: %w", err)
81 Headers: map[string]string{
82 "Note": dkimKeyNote("ed25519", selector, domain),
87 if err := pem.Encode(b, block); err != nil {
88 return nil, fmt.Errorf("encoding pem: %w", err)
93func dkimKeyNote(kind string, selector, domain dns.Domain) string {
94 s := kind + " dkim private key"
96 if selector != zero && domain != zero {
97 s += fmt.Sprintf(" for %s._domainkey.%s", selector.ASCII, domain.ASCII)
99 s += fmt.Sprintf(", generated by mox on %s", time.Now().Format(time.RFC3339))
103// MakeDKIMRSAKey returns a PEM buffer containing an rsa key for use with
105// selector and domain can be empty. If not, they are used in the note.
106func MakeDKIMRSAKey(selector, domain dns.Domain) ([]byte, error) {
107 // 2048 bits seems reasonable in 2022, 1024 is on the low side, larger
108 // keys may not fit in UDP DNS response.
109 privKey, err := rsa.GenerateKey(cryptorand.Reader, 2048)
111 return nil, fmt.Errorf("generating key: %w", err)
114 pkcs8, err := x509.MarshalPKCS8PrivateKey(privKey)
116 return nil, fmt.Errorf("marshal key: %w", err)
121 Headers: map[string]string{
122 "Note": dkimKeyNote("rsa-2048", selector, domain),
127 if err := pem.Encode(b, block); err != nil {
128 return nil, fmt.Errorf("encoding pem: %w", err)
130 return b.Bytes(), nil
133// MakeAccountConfig returns a new account configuration for an email address.
134func MakeAccountConfig(addr smtp.Address) config.Account {
135 account := config.Account{
136 Domain: addr.Domain.Name(),
137 Destinations: map[string]config.Destination{
140 RejectsMailbox: "Rejects",
141 JunkFilter: &config.JunkFilter{
152 account.AutomaticJunkFlags.Enabled = true
153 account.AutomaticJunkFlags.JunkMailboxRegexp = "^(junk|spam)"
154 account.AutomaticJunkFlags.NeutralMailboxRegexp = "^(inbox|neutral|postmaster|dmarc|tlsrpt|rejects)"
155 account.SubjectPass.Period = 12 * time.Hour
159func writeFile(log mlog.Log, path string, data []byte) error {
160 os.MkdirAll(filepath.Dir(path), 0770)
162 f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0660)
164 return fmt.Errorf("creating file %s: %s", path, err)
169 log.Check(err, "closing file after error")
170 err = os.Remove(path)
171 log.Check(err, "removing file after error", slog.String("path", path))
174 if _, err := f.Write(data); err != nil {
175 return fmt.Errorf("writing file %s: %s", path, err)
177 if err := f.Close(); err != nil {
178 return fmt.Errorf("close file: %v", err)
184// MakeDomainConfig makes a new config for a domain, creating DKIM keys, using
185// accountName for DMARC and TLS reports.
186func MakeDomainConfig(ctx context.Context, domain, hostname dns.Domain, accountName string, withMTASTS bool) (config.Domain, []string, error) {
187 log := pkglog.WithContext(ctx)
190 year := now.Format("2006")
191 timestamp := now.Format("20060102T150405")
195 for _, p := range paths {
197 log.Check(err, "removing path for domain config", slog.String("path", p))
201 confDKIM := config.DKIM{
202 Selectors: map[string]config.Selector{},
205 addSelector := func(kind, name string, privKey []byte) error {
206 record := fmt.Sprintf("%s._domainkey.%s", name, domain.ASCII)
207 keyPath := filepath.Join("dkim", fmt.Sprintf("%s.%s.%s.privatekey.pkcs8.pem", record, timestamp, kind))
208 p := configDirPath(ConfigDynamicPath, keyPath)
209 if err := writeFile(log, p, privKey); err != nil {
212 paths = append(paths, p)
213 confDKIM.Selectors[name] = config.Selector{
216 // Messages in the wild have been observed with 2 hours and 1 year expiration.
218 PrivateKeyFile: keyPath,
223 addEd25519 := func(name string) error {
224 key, err := MakeDKIMEd25519Key(dns.Domain{ASCII: name}, domain)
226 return fmt.Errorf("making dkim ed25519 private key: %s", err)
228 return addSelector("ed25519", name, key)
231 addRSA := func(name string) error {
232 key, err := MakeDKIMRSAKey(dns.Domain{ASCII: name}, domain)
234 return fmt.Errorf("making dkim rsa private key: %s", err)
236 return addSelector("rsa2048", name, key)
239 if err := addEd25519(year + "a"); err != nil {
240 return config.Domain{}, nil, err
242 if err := addRSA(year + "b"); err != nil {
243 return config.Domain{}, nil, err
245 if err := addEd25519(year + "c"); err != nil {
246 return config.Domain{}, nil, err
248 if err := addRSA(year + "d"); err != nil {
249 return config.Domain{}, nil, err
252 // We sign with the first two. In case they are misused, the switch to the other
253 // keys is easy, just change the config. Operators should make the public key field
254 // of the misused keys empty in the DNS records to disable the misused keys.
255 confDKIM.Sign = []string{year + "a", year + "b"}
257 confDomain := config.Domain{
258 ClientSettingsDomain: "mail." + domain.Name(),
259 LocalpartCatchallSeparator: "+",
261 DMARC: &config.DMARC{
262 Account: accountName,
263 Localpart: "dmarc-reports",
266 TLSRPT: &config.TLSRPT{
267 Account: accountName,
268 Localpart: "tls-reports",
274 confDomain.MTASTS = &config.MTASTS{
275 PolicyID: time.Now().UTC().Format("20060102T150405"),
276 Mode: mtasts.ModeEnforce,
277 // We start out with 24 hour, and warn in the admin interface that users should
278 // increase it to weeks once the setup works.
279 MaxAge: 24 * time.Hour,
280 MX: []string{hostname.ASCII},
287 return confDomain, rpaths, nil
290// DKIMAdd adds a DKIM selector for a domain, generating a key and writing it to disk.
291func DKIMAdd(ctx context.Context, domain, selector dns.Domain, algorithm, hash string, headerRelaxed, bodyRelaxed, seal bool, headers []string, lifetime time.Duration) (rerr error) {
292 log := pkglog.WithContext(ctx)
295 log.Errorx("adding dkim key", rerr,
296 slog.Any("domain", domain),
297 slog.Any("selector", selector))
302 case "sha256", "sha1":
304 return fmt.Errorf("%w: unknown hash algorithm %q", ErrRequest, hash)
312 privKey, err = MakeDKIMRSAKey(selector, domain)
315 privKey, err = MakeDKIMEd25519Key(selector, domain)
318 err = fmt.Errorf("unknown algorithm")
321 return fmt.Errorf("%w: making dkim key: %v", ErrRequest, err)
324 // Only take lock now, we don't want to hold it while generating a key.
325 Conf.dynamicMutex.Lock()
326 defer Conf.dynamicMutex.Unlock()
329 d, ok := c.Domains[domain.Name()]
331 return fmt.Errorf("%w: domain does not exist", ErrRequest)
334 if _, ok := d.DKIM.Selectors[selector.Name()]; ok {
335 return fmt.Errorf("%w: selector already exists for domain", ErrRequest)
338 record := fmt.Sprintf("%s._domainkey.%s", selector.ASCII, domain.ASCII)
339 timestamp := time.Now().Format("20060102T150405")
340 keyPath := filepath.Join("dkim", fmt.Sprintf("%s.%s.%s.privatekey.pkcs8.pem", record, timestamp, kind))
341 p := configDirPath(ConfigDynamicPath, keyPath)
342 if err := writeFile(log, p, privKey); err != nil {
343 return fmt.Errorf("writing key file: %v", err)
347 if removePath != "" {
348 err := os.Remove(removePath)
349 log.Check(err, "removing path for dkim key", slog.String("path", removePath))
353 nsel := config.Selector{
355 Canonicalization: config.Canonicalization{
356 HeaderRelaxed: headerRelaxed,
357 BodyRelaxed: bodyRelaxed,
360 DontSealHeaders: !seal,
361 Expiration: lifetime.String(),
362 PrivateKeyFile: keyPath,
365 // All good, time to update the config.
367 nd.DKIM.Selectors = map[string]config.Selector{}
368 for name, osel := range d.DKIM.Selectors {
369 nd.DKIM.Selectors[name] = osel
371 nd.DKIM.Selectors[selector.Name()] = nsel
373 nc.Domains = map[string]config.Domain{}
374 for name, dom := range c.Domains {
375 nc.Domains[name] = dom
377 nc.Domains[domain.Name()] = nd
379 if err := writeDynamic(ctx, log, nc); err != nil {
380 return fmt.Errorf("writing domains.conf: %w", err)
383 log.Info("dkim key added", slog.Any("domain", domain), slog.Any("selector", selector))
384 removePath = "" // Prevent cleanup of key file.
388// DKIMRemove removes the selector from the domain, moving the key file out of the way.
389func DKIMRemove(ctx context.Context, domain, selector dns.Domain) (rerr error) {
390 log := pkglog.WithContext(ctx)
393 log.Errorx("removing dkim key", rerr,
394 slog.Any("domain", domain),
395 slog.Any("selector", selector))
399 Conf.dynamicMutex.Lock()
400 defer Conf.dynamicMutex.Unlock()
403 d, ok := c.Domains[domain.Name()]
405 return fmt.Errorf("%w: domain does not exist", ErrRequest)
408 sel, ok := d.DKIM.Selectors[selector.Name()]
410 return fmt.Errorf("%w: selector does not exist for domain", ErrRequest)
413 nsels := map[string]config.Selector{}
414 for name, sel := range d.DKIM.Selectors {
415 if name != selector.Name() {
419 nsign := make([]string, 0, len(d.DKIM.Sign))
420 for _, name := range d.DKIM.Sign {
421 if name != selector.Name() {
422 nsign = append(nsign, name)
427 nd.DKIM = config.DKIM{Selectors: nsels, Sign: nsign}
429 nc.Domains = map[string]config.Domain{}
430 for name, dom := range c.Domains {
431 nc.Domains[name] = dom
433 nc.Domains[domain.Name()] = nd
435 if err := writeDynamic(ctx, log, nc); err != nil {
436 return fmt.Errorf("writing domains.conf: %w", err)
439 // Move away a DKIM private key to a subdirectory "old". But only if
440 // not in use by other domains.
441 usedKeyPaths := gatherUsedKeysPaths(nc)
442 moveAwayKeys(log, map[string]config.Selector{selector.Name(): sel}, usedKeyPaths)
444 log.Info("dkim key removed", slog.Any("domain", domain), slog.Any("selector", selector))
448// DomainAdd adds the domain to the domains config, rewriting domains.conf and
451// accountName is used for DMARC/TLS report and potentially for the postmaster address.
452// If the account does not exist, it is created with localpart. Localpart must be
453// set only if the account does not yet exist.
454func DomainAdd(ctx context.Context, domain dns.Domain, accountName string, localpart smtp.Localpart) (rerr error) {
455 log := pkglog.WithContext(ctx)
458 log.Errorx("adding domain", rerr,
459 slog.Any("domain", domain),
460 slog.String("account", accountName),
461 slog.Any("localpart", localpart))
465 Conf.dynamicMutex.Lock()
466 defer Conf.dynamicMutex.Unlock()
469 if _, ok := c.Domains[domain.Name()]; ok {
470 return fmt.Errorf("%w: domain already present", ErrRequest)
473 // Compose new config without modifying existing data structures. If we fail, we
476 nc.Domains = map[string]config.Domain{}
477 for name, d := range c.Domains {
481 // Only enable mta-sts for domain if there is a listener with mta-sts.
483 for _, l := range Conf.Static.Listeners {
484 if l.MTASTSHTTPS.Enabled {
490 confDomain, cleanupFiles, err := MakeDomainConfig(ctx, domain, Conf.Static.HostnameDomain, accountName, withMTASTS)
492 return fmt.Errorf("preparing domain config: %v", err)
495 for _, f := range cleanupFiles {
497 log.Check(err, "cleaning up file after error", slog.String("path", f))
501 if _, ok := c.Accounts[accountName]; ok && localpart != "" {
502 return fmt.Errorf("%w: account already exists (leave localpart empty when using an existing account)", ErrRequest)
503 } else if !ok && localpart == "" {
504 return fmt.Errorf("%w: account does not yet exist (specify a localpart)", ErrRequest)
505 } else if accountName == "" {
506 return fmt.Errorf("%w: account name is empty", ErrRequest)
508 nc.Accounts[accountName] = MakeAccountConfig(smtp.Address{Localpart: localpart, Domain: domain})
509 } else if accountName != Conf.Static.Postmaster.Account {
510 nacc := nc.Accounts[accountName]
511 nd := map[string]config.Destination{}
512 for k, v := range nacc.Destinations {
515 pmaddr := smtp.Address{Localpart: "postmaster", Domain: domain}
516 nd[pmaddr.String()] = config.Destination{}
517 nacc.Destinations = nd
518 nc.Accounts[accountName] = nacc
521 nc.Domains[domain.Name()] = confDomain
523 if err := writeDynamic(ctx, log, nc); err != nil {
524 return fmt.Errorf("writing domains.conf: %w", err)
526 log.Info("domain added", slog.Any("domain", domain))
527 cleanupFiles = nil // All good, don't cleanup.
531// DomainRemove removes domain from the config, rewriting domains.conf.
533// No accounts are removed, also not when they still reference this domain.
534func DomainRemove(ctx context.Context, domain dns.Domain) (rerr error) {
535 log := pkglog.WithContext(ctx)
538 log.Errorx("removing domain", rerr, slog.Any("domain", domain))
542 Conf.dynamicMutex.Lock()
543 defer Conf.dynamicMutex.Unlock()
546 domConf, ok := c.Domains[domain.Name()]
548 return fmt.Errorf("%w: domain does not exist", ErrRequest)
551 // Compose new config without modifying existing data structures. If we fail, we
554 nc.Domains = map[string]config.Domain{}
556 for name, d := range c.Domains {
562 if err := writeDynamic(ctx, log, nc); err != nil {
563 return fmt.Errorf("writing domains.conf: %w", err)
566 // Move away any DKIM private keys to a subdirectory "old". But only if
567 // they are not in use by other domains.
568 usedKeyPaths := gatherUsedKeysPaths(nc)
569 moveAwayKeys(log, domConf.DKIM.Selectors, usedKeyPaths)
571 log.Info("domain removed", slog.Any("domain", domain))
575func gatherUsedKeysPaths(nc config.Dynamic) map[string]bool {
576 usedKeyPaths := map[string]bool{}
577 for _, dc := range nc.Domains {
578 for _, sel := range dc.DKIM.Selectors {
579 usedKeyPaths[filepath.Clean(sel.PrivateKeyFile)] = true
585func moveAwayKeys(log mlog.Log, sels map[string]config.Selector, usedKeyPaths map[string]bool) {
586 for _, sel := range sels {
587 if sel.PrivateKeyFile == "" || usedKeyPaths[filepath.Clean(sel.PrivateKeyFile)] {
590 src := ConfigDirPath(sel.PrivateKeyFile)
591 dst := ConfigDirPath(filepath.Join(filepath.Dir(sel.PrivateKeyFile), "old", filepath.Base(sel.PrivateKeyFile)))
592 _, err := os.Stat(dst)
594 err = fmt.Errorf("destination already exists")
595 } else if os.IsNotExist(err) {
596 os.MkdirAll(filepath.Dir(dst), 0770)
597 err = os.Rename(src, dst)
600 log.Errorx("renaming dkim private key file for removed domain", err, slog.String("src", src), slog.String("dst", dst))
605// DomainSave calls xmodify with a shallow copy of the domain config. xmodify
606// can modify the config, but must clone all referencing data it changes.
607// xmodify may employ panic-based error handling. After xmodify returns, the
608// modified config is verified, saved and takes effect.
609func DomainSave(ctx context.Context, domainName string, xmodify func(config *config.Domain) error) (rerr error) {
610 log := pkglog.WithContext(ctx)
613 log.Errorx("saving domain config", rerr)
617 Conf.dynamicMutex.Lock()
618 defer Conf.dynamicMutex.Unlock()
620 nc := Conf.Dynamic // Shallow copy.
621 dom, ok := nc.Domains[domainName] // dom is a shallow copy.
623 return fmt.Errorf("%w: domain not present", ErrRequest)
626 if err := xmodify(&dom); err != nil {
630 // Compose new config without modifying existing data structures. If we fail, we
632 nc.Domains = map[string]config.Domain{}
633 for name, d := range Conf.Dynamic.Domains {
636 nc.Domains[domainName] = dom
638 if err := writeDynamic(ctx, log, nc); err != nil {
639 return fmt.Errorf("writing domains.conf: %w", err)
642 log.Info("domain saved")
646// ConfigSave calls xmodify with a shallow copy of the dynamic config. xmodify
647// can modify the config, but must clone all referencing data it changes.
648// xmodify may employ panic-based error handling. After xmodify returns, the
649// modified config is verified, saved and takes effect.
650func ConfigSave(ctx context.Context, xmodify func(config *config.Dynamic)) (rerr error) {
651 log := pkglog.WithContext(ctx)
654 log.Errorx("saving config", rerr)
658 Conf.dynamicMutex.Lock()
659 defer Conf.dynamicMutex.Unlock()
661 nc := Conf.Dynamic // Shallow copy.
664 if err := writeDynamic(ctx, log, nc); err != nil {
665 return fmt.Errorf("writing domains.conf: %w", err)
668 log.Info("config saved")
672// todo: find a way to automatically create the dns records as it would greatly simplify setting up email for a domain. we could also dynamically make changes, e.g. providing grace periods after disabling a dkim key, only automatically removing the dkim dns key after a few days. but this requires some kind of api and authentication to the dns server. there doesn't appear to be a single commonly used api for dns management. each of the numerous cloud providers have their own APIs and rather large SKDs to use them. we don't want to link all of them in.
674// DomainRecords returns text lines describing DNS records required for configuring
677// If certIssuerDomainName is set, CAA records to limit TLS certificate issuance to
678// that caID will be suggested. If acmeAccountURI is also set, CAA records also
679// restricting issuance to that account ID will be suggested.
680func DomainRecords(domConf config.Domain, domain dns.Domain, hasDNSSEC bool, certIssuerDomainName, acmeAccountURI string) ([]string, error) {
682 h := Conf.Static.HostnameDomain.ASCII
684 // The first line with ";" is used by ../testdata/integration/moxacmepebble.sh and
685 // ../testdata/integration/moxmail2.sh for selecting DNS records
687 "; Time To Live of 5 minutes, may be recognized if importing as a zone file.",
688 "; Once your setup is working, you may want to increase the TTL.",
693 if public, ok := Conf.Static.Listeners["public"]; ok && public.TLS != nil && (len(public.TLS.HostPrivateRSA2048Keys) > 0 || len(public.TLS.HostPrivateECDSAP256Keys) > 0) {
694 records = append(records,
695 `; DANE: These records indicate that a remote mail server trying to deliver email`,
696 `; with SMTP (TCP port 25) must verify the TLS certificate with DANE-EE (3), based`,
697 `; on the certificate public key ("SPKI", 1) that is SHA2-256-hashed (1) to the`,
698 `; hexadecimal hash. DANE-EE verification means only the certificate or public`,
699 `; key is verified, not whether the certificate is signed by a (centralized)`,
700 `; certificate authority (CA), is expired, or matches the host name.`,
702 `; NOTE: Create the records below only once: They are for the machine, and apply`,
703 `; to all hosted domains.`,
706 records = append(records,
708 "; WARNING: Domain does not appear to be DNSSEC-signed. To enable DANE, first",
709 "; enable DNSSEC on your domain, then add the TLSA records. Records below have been",
713 addTLSA := func(privKey crypto.Signer) error {
714 spkiBuf, err := x509.MarshalPKIXPublicKey(privKey.Public())
716 return fmt.Errorf("marshal SubjectPublicKeyInfo for DANE record: %v", err)
718 sum := sha256.Sum256(spkiBuf)
719 tlsaRecord := adns.TLSA{
720 Usage: adns.TLSAUsageDANEEE,
721 Selector: adns.TLSASelectorSPKI,
722 MatchType: adns.TLSAMatchTypeSHA256,
727 s = fmt.Sprintf("_25._tcp.%-*s TLSA %s", 20+len(d)-len("_25._tcp."), h+".", tlsaRecord.Record())
729 s = fmt.Sprintf(";; _25._tcp.%-*s TLSA %s", 20+len(d)-len(";; _25._tcp."), h+".", tlsaRecord.Record())
731 records = append(records, s)
734 for _, privKey := range public.TLS.HostPrivateECDSAP256Keys {
735 if err := addTLSA(privKey); err != nil {
739 for _, privKey := range public.TLS.HostPrivateRSA2048Keys {
740 if err := addTLSA(privKey); err != nil {
744 records = append(records, "")
748 records = append(records,
749 "; For the machine, only needs to be created once, for the first domain added:",
751 "; SPF-allow host for itself, resulting in relaxed DMARC pass for (postmaster)",
752 "; messages (DSNs) sent from host:",
757 if d != h && Conf.Static.HostTLSRPT.ParsedLocalpart != "" {
760 Opaque: smtp.NewAddress(Conf.Static.HostTLSRPT.ParsedLocalpart, Conf.Static.HostnameDomain).Pack(false),
762 tlsrptr := tlsrpt.Record{Version: "TLSRPTv1", RUAs: [][]tlsrpt.RUA{{tlsrpt.RUA(uri.String())}}}
763 records = append(records,
764 "; For the machine, only needs to be created once, for the first domain added:",
766 "; Request reporting about success/failures of TLS connections to (MX) host, for DANE.",
767 fmt.Sprintf(`_smtp._tls.%-*s TXT "%s"`, 20+len(d)-len("_smtp._tls."), h+".", tlsrptr.String()),
772 records = append(records,
773 "; Deliver email for the domain to this host.",
774 fmt.Sprintf("%s. MX 10 %s.", d, h),
777 "; Outgoing messages will be signed with the first two DKIM keys. The other two",
778 "; configured for backup, switching to them is just a config change.",
780 var selectors []string
781 for name := range domConf.DKIM.Selectors {
782 selectors = append(selectors, name)
784 sort.Slice(selectors, func(i, j int) bool {
785 return selectors[i] < selectors[j]
787 for _, name := range selectors {
788 sel := domConf.DKIM.Selectors[name]
789 dkimr := dkim.Record{
791 Hashes: []string{"sha256"},
792 PublicKey: sel.Key.Public(),
794 if _, ok := sel.Key.(ed25519.PrivateKey); ok {
795 dkimr.Key = "ed25519"
796 } else if _, ok := sel.Key.(*rsa.PrivateKey); !ok {
797 return nil, fmt.Errorf("unrecognized private key for DKIM selector %q: %T", name, sel.Key)
799 txt, err := dkimr.Record()
801 return nil, fmt.Errorf("making DKIM DNS TXT record: %v", err)
805 records = append(records,
806 "; NOTE: The following strings must be added to DNS as single record.",
809 s := fmt.Sprintf("%s._domainkey.%s. TXT %s", name, d, TXTStrings(txt))
810 records = append(records, s)
813 dmarcr := dmarc.DefaultRecord
814 dmarcr.Policy = "reject"
815 if domConf.DMARC != nil {
818 Opaque: smtp.NewAddress(domConf.DMARC.ParsedLocalpart, domConf.DMARC.DNSDomain).Pack(false),
820 dmarcr.AggregateReportAddresses = []dmarc.URI{
821 {Address: uri.String(), MaxSize: 10, Unit: "m"},
824 records = append(records,
827 "; Specify the MX host is allowed to send for our domain and for itself (for DSNs).",
828 "; ~all means softfail for anything else, which is done instead of -all to prevent older",
829 "; mail servers from rejecting the message because they never get to looking for a dkim/dmarc pass.",
830 fmt.Sprintf(`%s. TXT "v=spf1 mx ~all"`, d),
833 "; Emails that fail the DMARC check (without aligned DKIM and without aligned SPF)",
834 "; should be rejected, and request reports. If you email through mailing lists that",
835 "; strip DKIM-Signature headers and don't rewrite the From header, you may want to",
836 "; set the policy to p=none.",
837 fmt.Sprintf(`_dmarc.%s. TXT "%s"`, d, dmarcr.String()),
841 if sts := domConf.MTASTS; sts != nil {
842 records = append(records,
843 "; Remote servers can use MTA-STS to verify our TLS certificate with the",
844 "; WebPKI pool of CA's (certificate authorities) when delivering over SMTP with",
846 fmt.Sprintf(`mta-sts.%s. CNAME %s.`, d, h),
847 fmt.Sprintf(`_mta-sts.%s. TXT "v=STSv1; id=%s"`, d, sts.PolicyID),
851 records = append(records,
852 "; Note: No MTA-STS to indicate TLS should be used. Either because disabled for the",
853 "; domain or because mox.conf does not have a listener with MTA-STS configured.",
858 if domConf.TLSRPT != nil {
861 Opaque: smtp.NewAddress(domConf.TLSRPT.ParsedLocalpart, domConf.TLSRPT.DNSDomain).Pack(false),
863 tlsrptr := tlsrpt.Record{Version: "TLSRPTv1", RUAs: [][]tlsrpt.RUA{{tlsrpt.RUA(uri.String())}}}
864 records = append(records,
865 "; Request reporting about TLS failures.",
866 fmt.Sprintf(`_smtp._tls.%s. TXT "%s"`, d, tlsrptr.String()),
871 if domConf.ClientSettingsDomain != "" && domConf.ClientSettingsDNSDomain != Conf.Static.HostnameDomain {
872 records = append(records,
873 "; Client settings will reference a subdomain of the hosted domain, making it",
874 "; easier to migrate to a different server in the future by not requiring settings",
875 "; in all clients to be updated.",
876 fmt.Sprintf(`%-*s CNAME %s.`, 20+len(d), domConf.ClientSettingsDNSDomain.ASCII+".", h),
881 records = append(records,
882 "; Autoconfig is used by Thunderbird. Autodiscover is (in theory) used by Microsoft.",
883 fmt.Sprintf(`autoconfig.%s. CNAME %s.`, d, h),
884 fmt.Sprintf(`_autodiscover._tcp.%s. SRV 0 1 443 %s.`, d, h),
888 "; For secure IMAP and submission autoconfig, point to mail host.",
889 fmt.Sprintf(`_imaps._tcp.%s. SRV 0 1 993 %s.`, d, h),
890 fmt.Sprintf(`_submissions._tcp.%s. SRV 0 1 465 %s.`, d, h),
893 "; Next records specify POP3 and non-TLS ports are not to be used.",
894 "; These are optional and safe to leave out (e.g. if you have to click a lot in a",
895 "; DNS admin web interface).",
896 fmt.Sprintf(`_imap._tcp.%s. SRV 0 1 143 .`, d),
897 fmt.Sprintf(`_submission._tcp.%s. SRV 0 1 587 .`, d),
898 fmt.Sprintf(`_pop3._tcp.%s. SRV 0 1 110 .`, d),
899 fmt.Sprintf(`_pop3s._tcp.%s. SRV 0 1 995 .`, d),
902 if certIssuerDomainName != "" {
904 records = append(records,
907 "; You could mark Let's Encrypt as the only Certificate Authority allowed to",
908 "; sign TLS certificates for your domain.",
909 fmt.Sprintf(`%s. CAA 0 issue "%s"`, d, certIssuerDomainName),
911 if acmeAccountURI != "" {
914 records = append(records,
916 "; Optionally limit certificates for this domain to the account ID and methods used by mox.",
917 fmt.Sprintf(`;; %s. CAA 0 issue "%s; accounturi=%s; validationmethods=tls-alpn-01,http-01"`, d, certIssuerDomainName, acmeAccountURI),
919 "; Or alternatively only limit for email-specific subdomains, so you can use",
920 "; other accounts/methods for other subdomains.",
921 fmt.Sprintf(`;; autoconfig.%s. CAA 0 issue "%s; accounturi=%s; validationmethods=tls-alpn-01,http-01"`, d, certIssuerDomainName, acmeAccountURI),
922 fmt.Sprintf(`;; mta-sts.%s. CAA 0 issue "%s; accounturi=%s; validationmethods=tls-alpn-01,http-01"`, d, certIssuerDomainName, acmeAccountURI),
924 if domConf.ClientSettingsDomain != "" && domConf.ClientSettingsDNSDomain != Conf.Static.HostnameDomain {
925 records = append(records,
926 fmt.Sprintf(`;; %-*s CAA 0 issue "%s; accounturi=%s; validationmethods=tls-alpn-01,http-01"`, 20-3+len(d), domConf.ClientSettingsDNSDomain.ASCII, certIssuerDomainName, acmeAccountURI),
929 if strings.HasSuffix(h, "."+d) {
930 records = append(records,
932 "; And the mail hostname.",
933 fmt.Sprintf(`;; %-*s CAA 0 issue "%s; accounturi=%s; validationmethods=tls-alpn-01,http-01"`, 20-3+len(d), h+".", certIssuerDomainName, acmeAccountURI),
937 // The string "will be suggested" is used by
938 // ../testdata/integration/moxacmepebble.sh and ../testdata/integration/moxmail2.sh
939 // as end of DNS records.
940 records = append(records,
942 "; Note: After starting up, once an ACME account has been created, CAA records",
943 "; that restrict issuance to the account will be suggested.",
950// AccountAdd adds an account and an initial address and reloads the configuration.
952// The new account does not have a password, so cannot yet log in. Email can be
955// Catchall addresses are not supported for AccountAdd. Add separately with AddressAdd.
956func AccountAdd(ctx context.Context, account, address string) (rerr error) {
957 log := pkglog.WithContext(ctx)
960 log.Errorx("adding account", rerr, slog.String("account", account), slog.String("address", address))
964 addr, err := smtp.ParseAddress(address)
966 return fmt.Errorf("%w: parsing email address: %v", ErrRequest, err)
969 Conf.dynamicMutex.Lock()
970 defer Conf.dynamicMutex.Unlock()
973 if _, ok := c.Accounts[account]; ok {
974 return fmt.Errorf("%w: account already present", ErrRequest)
977 if err := checkAddressAvailable(addr); err != nil {
978 return fmt.Errorf("%w: address not available: %v", ErrRequest, err)
981 // Compose new config without modifying existing data structures. If we fail, we
984 nc.Accounts = map[string]config.Account{}
985 for name, a := range c.Accounts {
986 nc.Accounts[name] = a
988 nc.Accounts[account] = MakeAccountConfig(addr)
990 if err := writeDynamic(ctx, log, nc); err != nil {
991 return fmt.Errorf("writing domains.conf: %w", err)
993 log.Info("account added", slog.String("account", account), slog.Any("address", addr))
997// AccountRemove removes an account and reloads the configuration.
998func AccountRemove(ctx context.Context, account string) (rerr error) {
999 log := pkglog.WithContext(ctx)
1002 log.Errorx("adding account", rerr, slog.String("account", account))
1006 Conf.dynamicMutex.Lock()
1007 defer Conf.dynamicMutex.Unlock()
1010 if _, ok := c.Accounts[account]; !ok {
1011 return fmt.Errorf("%w: account does not exist", ErrRequest)
1014 // Compose new config without modifying existing data structures. If we fail, we
1017 nc.Accounts = map[string]config.Account{}
1018 for name, a := range c.Accounts {
1019 if name != account {
1020 nc.Accounts[name] = a
1024 if err := writeDynamic(ctx, log, nc); err != nil {
1025 return fmt.Errorf("writing domains.conf: %w", err)
1027 log.Info("account removed", slog.String("account", account))
1031// checkAddressAvailable checks that the address after canonicalization is not
1032// already configured, and that its localpart does not contain the catchall
1033// localpart separator.
1035// Must be called with config lock held.
1036func checkAddressAvailable(addr smtp.Address) error {
1037 dc, ok := Conf.Dynamic.Domains[addr.Domain.Name()]
1039 return fmt.Errorf("domain does not exist")
1041 lp := CanonicalLocalpart(addr.Localpart, dc)
1042 if _, ok := Conf.accountDestinations[smtp.NewAddress(lp, addr.Domain).String()]; ok {
1043 return fmt.Errorf("canonicalized address %s already configured", smtp.NewAddress(lp, addr.Domain))
1044 } else if dc.LocalpartCatchallSeparator != "" && strings.Contains(string(addr.Localpart), dc.LocalpartCatchallSeparator) {
1045 return fmt.Errorf("localpart cannot include domain catchall separator %s", dc.LocalpartCatchallSeparator)
1046 } else if _, ok := dc.Aliases[lp.String()]; ok {
1047 return fmt.Errorf("address in use as alias")
1052// AddressAdd adds an email address to an account and reloads the configuration. If
1053// address starts with an @ it is treated as a catchall address for the domain.
1054func AddressAdd(ctx context.Context, address, account string) (rerr error) {
1055 log := pkglog.WithContext(ctx)
1058 log.Errorx("adding address", rerr, slog.String("address", address), slog.String("account", account))
1062 Conf.dynamicMutex.Lock()
1063 defer Conf.dynamicMutex.Unlock()
1066 a, ok := c.Accounts[account]
1068 return fmt.Errorf("%w: account does not exist", ErrRequest)
1072 if strings.HasPrefix(address, "@") {
1073 d, err := dns.ParseDomain(address[1:])
1075 return fmt.Errorf("%w: parsing domain: %v", ErrRequest, err)
1078 destAddr = "@" + dname
1079 if _, ok := Conf.Dynamic.Domains[dname]; !ok {
1080 return fmt.Errorf("%w: domain does not exist", ErrRequest)
1081 } else if _, ok := Conf.accountDestinations[destAddr]; ok {
1082 return fmt.Errorf("%w: catchall address already configured for domain", ErrRequest)
1085 addr, err := smtp.ParseAddress(address)
1087 return fmt.Errorf("%w: parsing email address: %v", ErrRequest, err)
1090 if err := checkAddressAvailable(addr); err != nil {
1091 return fmt.Errorf("%w: address not available: %v", ErrRequest, err)
1093 destAddr = addr.String()
1096 // Compose new config without modifying existing data structures. If we fail, we
1099 nc.Accounts = map[string]config.Account{}
1100 for name, a := range c.Accounts {
1101 nc.Accounts[name] = a
1103 nd := map[string]config.Destination{}
1104 for name, d := range a.Destinations {
1107 nd[destAddr] = config.Destination{}
1109 nc.Accounts[account] = a
1111 if err := writeDynamic(ctx, log, nc); err != nil {
1112 return fmt.Errorf("writing domains.conf: %w", err)
1114 log.Info("address added", slog.String("address", address), slog.String("account", account))
1118// AddressRemove removes an email address and reloads the configuration.
1119// Address can be a catchall address for the domain of the form "@<domain>".
1121// If the address is member of an alias, remove it from from the alias, unless it
1122// is the last member.
1123func AddressRemove(ctx context.Context, address string) (rerr error) {
1124 log := pkglog.WithContext(ctx)
1127 log.Errorx("removing address", rerr, slog.String("address", address))
1131 Conf.dynamicMutex.Lock()
1132 defer Conf.dynamicMutex.Unlock()
1134 ad, ok := Conf.accountDestinations[address]
1136 return fmt.Errorf("%w: address does not exists", ErrRequest)
1139 // Compose new config without modifying existing data structures. If we fail, we
1141 a, ok := Conf.Dynamic.Accounts[ad.Account]
1143 return fmt.Errorf("internal error: cannot find account")
1146 na.Destinations = map[string]config.Destination{}
1148 for destAddr, d := range a.Destinations {
1149 if destAddr != address {
1150 na.Destinations[destAddr] = d
1156 return fmt.Errorf("%w: address not removed, likely a postmaster/reporting address", ErrRequest)
1159 // Also remove matching address from FromIDLoginAddresses, composing a new slice.
1160 var fromIDLoginAddresses []string
1162 var pa smtp.Address // For non-catchall addresses (most).
1164 if strings.HasPrefix(address, "@") {
1165 dom, err = dns.ParseDomain(address[1:])
1167 return fmt.Errorf("%w: parsing domain for catchall address: %v", ErrRequest, err)
1170 pa, err = smtp.ParseAddress(address)
1172 return fmt.Errorf("%w: parsing address: %v", ErrRequest, err)
1176 for i, fa := range a.ParsedFromIDLoginAddresses {
1177 if fa.Domain != dom {
1178 // Keep for different domain.
1179 fromIDLoginAddresses = append(fromIDLoginAddresses, a.FromIDLoginAddresses[i])
1182 if strings.HasPrefix(address, "@") {
1185 dc, ok := Conf.Dynamic.Domains[dom.Name()]
1187 return fmt.Errorf("%w: unknown domain in fromid login address %q", ErrRequest, fa.Pack(true))
1189 flp := CanonicalLocalpart(fa.Localpart, dc)
1190 alp := CanonicalLocalpart(pa.Localpart, dc)
1192 // Keep for different localpart.
1193 fromIDLoginAddresses = append(fromIDLoginAddresses, a.FromIDLoginAddresses[i])
1196 na.FromIDLoginAddresses = fromIDLoginAddresses
1198 // And remove as member from aliases configured in domains.
1199 domains := maps.Clone(Conf.Dynamic.Domains)
1200 for _, aa := range na.Aliases {
1201 if aa.SubscriptionAddress != address {
1205 aliasAddr := fmt.Sprintf("%s@%s", aa.Alias.LocalpartStr, aa.Alias.Domain.Name())
1207 dom, ok := Conf.Dynamic.Domains[aa.Alias.Domain.Name()]
1209 return fmt.Errorf("cannot find domain for alias %s", aliasAddr)
1211 a, ok := dom.Aliases[aa.Alias.LocalpartStr]
1213 return fmt.Errorf("cannot find alias %s", aliasAddr)
1215 a.Addresses = slices.Clone(a.Addresses)
1216 a.Addresses = slices.DeleteFunc(a.Addresses, func(v string) bool { return v == address })
1217 if len(a.Addresses) == 0 {
1218 return fmt.Errorf("address is last member of alias %s, add new members or remove alias first", aliasAddr)
1220 a.ParsedAddresses = nil // Filled when parsing config.
1221 dom.Aliases = maps.Clone(dom.Aliases)
1222 dom.Aliases[aa.Alias.LocalpartStr] = a
1223 domains[aa.Alias.Domain.Name()] = dom
1225 na.Aliases = nil // Filled when parsing config.
1228 nc.Accounts = map[string]config.Account{}
1229 for name, a := range Conf.Dynamic.Accounts {
1230 nc.Accounts[name] = a
1232 nc.Accounts[ad.Account] = na
1233 nc.Domains = domains
1235 if err := writeDynamic(ctx, log, nc); err != nil {
1236 return fmt.Errorf("writing domains.conf: %w", err)
1238 log.Info("address removed", slog.String("address", address), slog.String("account", ad.Account))
1242func AliasAdd(ctx context.Context, addr smtp.Address, alias config.Alias) error {
1243 return DomainSave(ctx, addr.Domain.Name(), func(d *config.Domain) error {
1244 if _, ok := d.Aliases[addr.Localpart.String()]; ok {
1245 return fmt.Errorf("%w: alias already present", ErrRequest)
1247 if d.Aliases == nil {
1248 d.Aliases = map[string]config.Alias{}
1250 d.Aliases = maps.Clone(d.Aliases)
1251 d.Aliases[addr.Localpart.String()] = alias
1256func AliasUpdate(ctx context.Context, addr smtp.Address, alias config.Alias) error {
1257 return DomainSave(ctx, addr.Domain.Name(), func(d *config.Domain) error {
1258 a, ok := d.Aliases[addr.Localpart.String()]
1260 return fmt.Errorf("%w: alias does not exist", ErrRequest)
1262 a.PostPublic = alias.PostPublic
1263 a.ListMembers = alias.ListMembers
1264 a.AllowMsgFrom = alias.AllowMsgFrom
1265 d.Aliases = maps.Clone(d.Aliases)
1266 d.Aliases[addr.Localpart.String()] = a
1271func AliasRemove(ctx context.Context, addr smtp.Address) error {
1272 return DomainSave(ctx, addr.Domain.Name(), func(d *config.Domain) error {
1273 _, ok := d.Aliases[addr.Localpart.String()]
1275 return fmt.Errorf("%w: alias does not exist", ErrRequest)
1277 d.Aliases = maps.Clone(d.Aliases)
1278 delete(d.Aliases, addr.Localpart.String())
1283func AliasAddressesAdd(ctx context.Context, addr smtp.Address, addresses []string) error {
1284 if len(addresses) == 0 {
1285 return fmt.Errorf("%w: at least one address required", ErrRequest)
1287 return DomainSave(ctx, addr.Domain.Name(), func(d *config.Domain) error {
1288 alias, ok := d.Aliases[addr.Localpart.String()]
1290 return fmt.Errorf("%w: no such alias", ErrRequest)
1292 alias.Addresses = append(slices.Clone(alias.Addresses), addresses...)
1293 alias.ParsedAddresses = nil
1294 d.Aliases = maps.Clone(d.Aliases)
1295 d.Aliases[addr.Localpart.String()] = alias
1300func AliasAddressesRemove(ctx context.Context, addr smtp.Address, addresses []string) error {
1301 if len(addresses) == 0 {
1302 return fmt.Errorf("%w: need at least one address", ErrRequest)
1304 return DomainSave(ctx, addr.Domain.Name(), func(d *config.Domain) error {
1305 alias, ok := d.Aliases[addr.Localpart.String()]
1307 return fmt.Errorf("%w: no such alias", ErrRequest)
1309 alias.Addresses = slices.DeleteFunc(slices.Clone(alias.Addresses), func(addr string) bool {
1311 addresses = slices.DeleteFunc(addresses, func(a string) bool { return a == addr })
1312 return n > len(addresses)
1314 if len(addresses) > 0 {
1315 return fmt.Errorf("%w: address not found: %s", ErrRequest, strings.Join(addresses, ", "))
1317 alias.ParsedAddresses = nil
1318 d.Aliases = maps.Clone(d.Aliases)
1319 d.Aliases[addr.Localpart.String()] = alias
1324// AccountSave updates the configuration of an account. Function xmodify is called
1325// with a shallow copy of the current configuration of the account. It must not
1326// change referencing fields (e.g. existing slice/map/pointer), they may still be
1327// in use, and the change may be rolled back. Referencing values must be copied and
1328// replaced by the modify. The function may raise a panic for error handling.
1329func AccountSave(ctx context.Context, account string, xmodify func(acc *config.Account)) (rerr error) {
1330 log := pkglog.WithContext(ctx)
1333 log.Errorx("saving account fields", rerr, slog.String("account", account))
1337 Conf.dynamicMutex.Lock()
1338 defer Conf.dynamicMutex.Unlock()
1341 acc, ok := c.Accounts[account]
1343 return fmt.Errorf("%w: account not present", ErrRequest)
1348 // Compose new config without modifying existing data structures. If we fail, we
1351 nc.Accounts = map[string]config.Account{}
1352 for name, a := range c.Accounts {
1353 nc.Accounts[name] = a
1355 nc.Accounts[account] = acc
1357 if err := writeDynamic(ctx, log, nc); err != nil {
1358 return fmt.Errorf("writing domains.conf: %w", err)
1360 log.Info("account fields saved", slog.String("account", account))
1367 TLSModeImmediate TLSMode = 0
1368 TLSModeSTARTTLS TLSMode = 1
1369 TLSModeNone TLSMode = 2
1372type ProtocolConfig struct {
1378type ClientConfig struct {
1380 Submission ProtocolConfig
1383// ClientConfigDomain returns a single IMAP and Submission client configuration for
1385func ClientConfigDomain(d dns.Domain) (rconfig ClientConfig, rerr error) {
1386 var haveIMAP, haveSubmission bool
1388 domConf, ok := Conf.Domain(d)
1390 return ClientConfig{}, fmt.Errorf("%w: unknown domain", ErrRequest)
1393 gather := func(l config.Listener) (done bool) {
1394 host := Conf.Static.HostnameDomain
1395 if l.Hostname != "" {
1396 host = l.HostnameDomain
1398 if domConf.ClientSettingsDomain != "" {
1399 host = domConf.ClientSettingsDNSDomain
1401 if !haveIMAP && l.IMAPS.Enabled {
1402 rconfig.IMAP.Host = host
1403 rconfig.IMAP.Port = config.Port(l.IMAPS.Port, 993)
1404 rconfig.IMAP.TLSMode = TLSModeImmediate
1407 if !haveIMAP && l.IMAP.Enabled {
1408 rconfig.IMAP.Host = host
1409 rconfig.IMAP.Port = config.Port(l.IMAP.Port, 143)
1410 rconfig.IMAP.TLSMode = TLSModeSTARTTLS
1412 rconfig.IMAP.TLSMode = TLSModeNone
1416 if !haveSubmission && l.Submissions.Enabled {
1417 rconfig.Submission.Host = host
1418 rconfig.Submission.Port = config.Port(l.Submissions.Port, 465)
1419 rconfig.Submission.TLSMode = TLSModeImmediate
1420 haveSubmission = true
1422 if !haveSubmission && l.Submission.Enabled {
1423 rconfig.Submission.Host = host
1424 rconfig.Submission.Port = config.Port(l.Submission.Port, 587)
1425 rconfig.Submission.TLSMode = TLSModeSTARTTLS
1427 rconfig.Submission.TLSMode = TLSModeNone
1429 haveSubmission = true
1431 return haveIMAP && haveSubmission
1434 // Look at the public listener first. Most likely the intended configuration.
1435 if public, ok := Conf.Static.Listeners["public"]; ok {
1440 // Go through the other listeners in consistent order.
1441 names := maps.Keys(Conf.Static.Listeners)
1443 for _, name := range names {
1444 if gather(Conf.Static.Listeners[name]) {
1448 return ClientConfig{}, fmt.Errorf("%w: no listeners found for imap and/or submission", ErrRequest)
1451// ClientConfigs holds the client configuration for IMAP/Submission for a
1453type ClientConfigs struct {
1454 Entries []ClientConfigsEntry
1457type ClientConfigsEntry struct {
1465// ClientConfigsDomain returns the client configs for IMAP/Submission for a
1467func ClientConfigsDomain(d dns.Domain) (ClientConfigs, error) {
1468 domConf, ok := Conf.Domain(d)
1470 return ClientConfigs{}, fmt.Errorf("%w: unknown domain", ErrRequest)
1473 c := ClientConfigs{}
1474 c.Entries = []ClientConfigsEntry{}
1475 var listeners []string
1477 for name := range Conf.Static.Listeners {
1478 listeners = append(listeners, name)
1480 sort.Slice(listeners, func(i, j int) bool {
1481 return listeners[i] < listeners[j]
1484 note := func(tls bool, requiretls bool) string {
1486 return "plain text, no STARTTLS configured"
1489 return "STARTTLS required"
1491 return "STARTTLS optional"
1494 for _, name := range listeners {
1495 l := Conf.Static.Listeners[name]
1496 host := Conf.Static.HostnameDomain
1497 if l.Hostname != "" {
1498 host = l.HostnameDomain
1500 if domConf.ClientSettingsDomain != "" {
1501 host = domConf.ClientSettingsDNSDomain
1503 if l.Submissions.Enabled {
1504 c.Entries = append(c.Entries, ClientConfigsEntry{"Submission (SMTP)", host, config.Port(l.Submissions.Port, 465), name, "with TLS"})
1506 if l.IMAPS.Enabled {
1507 c.Entries = append(c.Entries, ClientConfigsEntry{"IMAP", host, config.Port(l.IMAPS.Port, 993), name, "with TLS"})
1509 if l.Submission.Enabled {
1510 c.Entries = append(c.Entries, ClientConfigsEntry{"Submission (SMTP)", host, config.Port(l.Submission.Port, 587), name, note(l.TLS != nil, !l.Submission.NoRequireSTARTTLS)})
1513 c.Entries = append(c.Entries, ClientConfigsEntry{"IMAP", host, config.Port(l.IMAPS.Port, 143), name, note(l.TLS != nil, !l.IMAP.NoRequireSTARTTLS)})
1520// IPs returns ip addresses we may be listening/receiving mail on or
1521// connecting/sending from to the outside.
1522func IPs(ctx context.Context, receiveOnly bool) ([]net.IP, error) {
1523 log := pkglog.WithContext(ctx)
1525 // Try to gather all IPs we are listening on by going through the config.
1526 // If we encounter 0.0.0.0 or ::, we'll gather all local IPs afterwards.
1528 var ipv4all, ipv6all bool
1529 for _, l := range Conf.Static.Listeners {
1530 // If NATed, we don't know our external IPs.
1535 if len(l.NATIPs) > 0 {
1538 for _, s := range check {
1539 ip := net.ParseIP(s)
1540 if ip.IsUnspecified() {
1541 if ip.To4() != nil {
1548 ips = append(ips, ip)
1552 // We'll list the IPs on the interfaces. How useful is this? There is a good chance
1553 // we're listening on all addresses because of a load balancer/firewall.
1554 if ipv4all || ipv6all {
1555 ifaces, err := net.Interfaces()
1557 return nil, fmt.Errorf("listing network interfaces: %v", err)
1559 for _, iface := range ifaces {
1560 if iface.Flags&net.FlagUp == 0 {
1563 addrs, err := iface.Addrs()
1565 return nil, fmt.Errorf("listing addresses for network interface: %v", err)
1567 if len(addrs) == 0 {
1571 for _, addr := range addrs {
1572 ip, _, err := net.ParseCIDR(addr.String())
1574 log.Errorx("bad interface addr", err, slog.Any("address", addr))
1577 v4 := ip.To4() != nil
1578 if ipv4all && v4 || ipv6all && !v4 {
1579 ips = append(ips, ip)
1589 for _, t := range Conf.Static.Transports {
1591 ips = append(ips, t.Socks.IPs...)